const Path = require('path'); const Fs = require('fire-fs'); const CfgUtil = Editor.require('packages://hot-update-tools/core/CfgUtil.js'); const Util = Editor.require('packages://hot-update-tools/core/Util.js'); const OutPut = Editor.require('packages://hot-update-tools/core/OutPut.js'); const GoogleAnalytics = Editor.require( 'packages://hot-update-tools/core/GoogleAnalytics.js', ); const Electron = require('electron'); const { write } = require('fs'); const FsExtra = require('fs-extra'); Vue.component('manifest-gen', { template: Fs.readFileSync( Editor.url('packages://hot-update-tools/panel/manifest-gen.html'), 'utf-8', ), mixins: [Editor.require('packages://hot-update-tools/panel/mixin.js')], data() { return { gameName: '', whiteList: '', version: '', serverRootDir: '', remoteServerVersion: '', // 远程服务器版本 resourceRootDir: '', genManifestDir: '', // 热更资源服务器配置历史记录 isShowUseAddrBtn: false, isShowDelAddrBtn: false, subPackArr: {}, allProjectManifest: {} }; }, computed: { isValidResDir() { return !!(this.resourceRootDir && Fs.existsSync(this.resourceRootDir)); }, }, created() { this.$nextTick(() => { let data = CfgUtil.cfgData; if (data) { this.gameName = data.gameName || 'assets'; this.whiteList = data.whiteList || 'internal,main,resources'; this.subPackArr = data.subPackArr || {}; this.serverRootDir = data.serverRootDir; this.resourceRootDir = data.resourceRootDir; let keys = Object.keys(this.subPackArr); for (let i = 0; i < keys.length; i += 1) { let index = keys[i]; let data = this.subPackArr[index]; data.versionNumber = "正在从远端获取"; this._getServerVersion(index, (version) => { data.versionNumber = version; this._saveConfig(); }); } } this.genManifestDir = OutPut.manifestDir; this._getServerVersion(this.gameName, (version) => { this.remoteServerVersion = version; }); this._initResourceBuild(); }); }, methods: { _initResourceBuild() { let projectDir = Editor.Project.path; let buildCfg = Path.join(projectDir, 'local/builder.json'); if (Fs.existsSync(buildCfg)) { let buildData = JSON.parse(Fs.readFileSync(buildCfg, 'utf-8')); let buildDir = buildData.buildPath; let buildFullDir = Path.join(projectDir, buildDir); let packResourceDir = Path.join( buildFullDir, `jsb-${buildData.template}`, ); if (!Fs.existsSync(packResourceDir)) { let platformDir = Path.join(buildFullDir, buildData.platform); if (Fs.existsSync(platformDir)) { packResourceDir = platformDir; } } this._checkResourceRootDir(packResourceDir); } else { this.log('发现没有构建项目, 使用前请先构建项目!'); } }, _isVersionPass(newVersion, baseVersion) { if ( newVersion === undefined || newVersion === null || baseVersion === undefined || baseVersion === null ) { return false; } let arrayA = newVersion.split('.'); let arrayB = baseVersion.split('.'); let len = arrayA.length > arrayB.length ? arrayA.length : arrayB.length; for (let i = 0; i < len; i++) { let itemA = arrayA[i]; let itemB = arrayB[i]; // 新版本1.2 老版本 1.2.3 if (itemA === undefined && itemB !== undefined) { return false; } // 新版本1.2.1, 老版本1.2 if (itemA !== undefined && itemB === undefined) { return true; } if (itemA && itemB && parseInt(itemA) > parseInt(itemB)) { return true; } } return false; }, _updateShowUseAddrBtn() { // let selectURL = this.$els.address.value; // if (this.serverRootDir === selectURL) { // this.isShowUseAddrBtn = false; // } }, _getServerVersion(gameName, callFunc) { if (this.serverRootDir.length <= 0) { console.log('远程资源服务器URL错误: ' + this.serverRootDir); return; } let versionUrl = this.serverRootDir + '/' + gameName; if (gameName != 'assets') { versionUrl += '/' + gameName + '_project.manifest'; } else { versionUrl += '/' + 'project.manifest'; } let xhr = new XMLHttpRequest(); xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 400) { let text = xhr.responseText; let json = null; try { json = JSON.parse(text); } catch (e) { console.log(e); this.log('获取远程版本号失败!'); return; } this.allProjectManifest[gameName] = json; callFunc && callFunc(json.version); } else if (xhr.status === 404 || xhr.status === 0) { callFunc && callFunc("远端地址错误"); } }; xhr.open('get', versionUrl, true); xhr.setRequestHeader('If-Modified-Since', '0'); // 不缓存 xhr.send(); }, onClickGenCfg(event) { GoogleAnalytics.eventCustom('GenManifest'); // 检查是否需要构建项目 // let times = CfgUtil.getBuildTimeGenTime(); // let genTime = times.genTime; // let buildTime = times.buildTime; // if (genTime === buildTime) {// 构建完版本之后没有生成manifest文件 // CfgUtil.updateGenTime(new Date().getTime(), this.version);// 更新生成时间 // } else { // this.log('[生成] 你需要重新构建项目,因为上次构建已经和版本关联:' + CfgUtil.cfgData.genVersion); // return; // } if (!this.serverRootDir || this.serverRootDir.length <= 0) { this.log('[生成] 服务器地址未填写'); return; } // 检查resource目录 if (this.resourceRootDir.length === 0) { this.log('[生成] 请先指定 '); return; } if (!this._checkResourceRootDir(this.resourceRootDir)) { return; } if (!this.genManifestDir || this.genManifestDir.length <= 0) { this.log('[生成] manifest文件生成地址未填写'); return; } if (!Fs.existsSync(this.genManifestDir)) { this.log('[生成] manifest存储目录不存在: ' + this.genManifestDir); return; } this._saveConfig(); this._genVersion2( this.serverRootDir, this.resourceRootDir, this.genManifestDir, ); }, onClickOpenVersionDir() { this.openDir(OutPut.versionsDir); }, onOpenManifestDir() { this.openDir(this.genManifestDir); }, onOpenResourceDir() { this.openDir(this.resourceRootDir); }, onSelectResourceRootDir() { let res = Editor.Dialog.openFile({ title: '选择构建后的根目录', defaultPath: Editor.projectInfo.path, properties: ['openDirectory'], }); if (res !== -1) { let dir = res[0]; if (this._checkResourceRootDir(dir)) { this.resourceRootDir = dir; this._saveConfig(); } } }, // 删除热更历史地址 onBtnClickDelSelectedHotAddress() { let address = this.$els.address.value; }, onBtnClickUseSelectedHotAddress() { this.$els; let address = this.$els.address.value; this.serverRootDir = address; this.onInPutUrlOver(); this._updateShowUseAddrBtn(); }, onChangeSelectHotAddress(event) { console.log('change'); GoogleAnalytics.eventCustom('ChangeSelectHotAddress'); this.isShowUseAddrBtn = true; this.isShowDelAddrBtn = true; this._updateShowUseAddrBtn(); }, userLocalIP() { GoogleAnalytics.eventCustom('useLocalIP'); const Util = Editor.require('packages://hot-update-tools/core/Util.js'); let ip = Util.getLocalIP(); if (ip.length > 0) { this.serverRootDir = 'http://' + ip; this.onInPutUrlOver(null); } }, onInPutUrlOver(event) { let url = this.serverRootDir; if ( url === 'http://' || url === 'https://' || url === 'http' || url === 'https' || url === 'http:' || url === 'https:' ) { return; } let index1 = url.indexOf('http://'); let index2 = url.indexOf('https://'); if (index1 === -1 && index2 === -1) { let reg = /^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(([A-Za-z0-9-~]+)\.)+([A-Za-z0-9-~\/])+$/; if (!reg.test(url)) { this.log( url + ' 不是以http://https://开头,或者不是网址, 已经自动修改', ); this.serverRootDir = 'http://' + this.serverRootDir; this._getServerVersion(this.gameName, (version) => { this.remoteServerVersion = version; }); } } else { // 更新服务器版本 this._getServerVersion(this.gameName, (version) => { this.remoteServerVersion = version; }); } this._updateShowUseAddrBtn(); this._saveConfig(); }, _saveConfig() { let data = { gameName: this.gameName, whiteList: this.whiteList, subPackArr: this.subPackArr, serverRootDir: this.serverRootDir, resourceRootDir: this.resourceRootDir, genManifestDir: OutPut.manifestDir, localServerPath: this.localServerPath, }; CfgUtil.saveConfig(data); }, onInputVersionOver() { let buildVersion = CfgUtil.cfgData.genVersion; let buildTime = CfgUtil.cfgData.buildTime; let genTime = CfgUtil.cfgData.genTime; // 生成manifest时间 let remoteVersion = this.remoteServerVersion; if (remoteVersion !== null && remoteVersion !== undefined) { // 存在远程版本 } else { // 未发现远程版本 } // let nowTime = new Date().getTime(); // if (nowTime !== buildTime) { // if (genVersion === this.version) { // this.log("版本一致,请构建项目"); // } else { // } // } this._saveConfig(); }, onStopTouchEvent(event) { event.preventDefault(); event.stopPropagation(); }, onBtnClickHelpDoc() { GoogleAnalytics.eventDoc(); // let url = 'https://tidys.github.io/plugin-docs-oneself/docs/hot-update-tools/'; const url = 'https://tidys.gitee.io/doc'; Electron.shell.openExternal(url); }, onBtnClickTellMe() { GoogleAnalytics.eventQQ(); let url = 'http://wpa.qq.com/msgrd?v=3&uin=774177933&site=qq&menu=yes'; Electron.shell.openExternal(url); }, // serverUrl 必须以/结尾 // genManifestDir 建议在assets目录下 // buildResourceDir 默认为 build/jsb-default/ // -v 10.1.1 -u http://192.168.191.1//cocos/remote-assets/ -s build/jsb-default/ -d assets _genVersion2(serverUrl, buildResourceDir, genManifestDir) { this.log('[Build] 开始生成manifest配置文件....'); let dest = genManifestDir; let src = buildResourceDir; let readDir = (dir, obj, bundleName) => { let stat = Fs.statSync(dir); if (!stat.isDirectory()) { return; } let subpaths = Fs.readdirSync(dir); let subpath; let size; let md5; let compressed; let relative; for (let i = 0; i < subpaths.length; ++i) { if (subpaths[i][0] === '.') { continue; } subpath = Path.join(dir, subpaths[i]); stat = Fs.statSync(subpath); if (stat.isDirectory()) { readDir(subpath, obj, bundleName); } else if (stat.isFile()) { // Size in Bytes size = stat['size']; // let crypto = require('crypto'); md5 = require('crypto') .createHash('md5') .update(Fs.readFileSync(subpath)) .digest('hex'); compressed = Path.extname(subpath).toLowerCase() === '.zip'; relative = Path.relative(src, subpath); relative = relative.replace(/\\/g, '/'); relative = encodeURI(relative); let splitData1 = relative.split("/"); if (splitData1[1] == bundleName) { let splitData2 = relative.split("/" + splitData1[1]); relative = splitData2[0] + splitData2[1]; } obj[relative] = { size: size, md5: md5, }; if (compressed) { obj[relative].compressed = true; } } } }; let mkdirSync = (path) => { try { Fs.mkdirSync(path); } catch (e) { if (e.code !== 'EEXIST') throw e; } }; let createManifest = (version, serverUrl, gameName) => { let versionUrl = this.serverRootDir + '/' + gameName; let manifestUrl = this.serverRootDir + '/' + gameName; if (gameName != 'assets') { versionUrl += '/' + gameName + '_version.manifest'; manifestUrl += '/' + gameName + '_project.manifest'; } else { versionUrl += '/' + 'version.manifest'; manifestUrl += '/' + 'project.manifest'; } let manifest = { version: (version == "远端地址错误" ? "0" : (parseInt(version) + 1) + ''), packageUrl: serverUrl, remoteManifestUrl: manifestUrl, remoteVersionUrl: versionUrl, assets: {}, searchPaths: [], }; return manifest; } let checkMd5 = (manifest, name) => { let newAssets = manifest.assets; let oldMAssets = this.allProjectManifest[name] ? this.allProjectManifest[name].assets : {}; let keys = Object.keys(newAssets); for (let i = 0; i < keys.length; i += 1) { let index = keys[i]; let newFile = newAssets[index]; let oldFile = oldMAssets[index]; if (!oldFile) { return true; } if (newFile.md5 != oldFile.md5) { return true; } } return false; } let writeToManifest = (manifest, bundleName, underline) => { let path = Path.join(dest, bundleName); let destManifest = Path.join(path, bundleName + underline + 'project.manifest'); let destVersion = Path.join(path, bundleName + underline + 'version.manifest'); if (!Fs.existsSync(path)) { FsExtra.ensureDirSync(path); } mkdirSync(dest); // 生成project.manifest Fs.writeFileSync(destManifest, JSON.stringify(manifest)); // 生成version.manifest delete manifest.assets; delete manifest.searchPaths; Fs.writeFileSync(destVersion, JSON.stringify(manifest)); } //拆分bundle与assets let checkArr = this.whiteList.split(',') let bundlePath = []; //bundle let keys = Object.keys(this.subPackArr); for (let i = 0; i < keys.length; i += 1) { let name = keys[i]; let data = this.subPackArr[name]; let manifest = createManifest(data.versionNumber, data.subPackUrl, name); readDir(Path.join(src, Util.manifestResDir, name), manifest.assets, name); if (checkMd5(manifest, name)) { writeToManifest(manifest, name, "_"); bundlePath.push(name); } } //assets src单独处理 let manifest = createManifest(this.remoteServerVersion, this.serverRootDir + '/' + this.gameName, this.gameName); for (let i = 0; i < checkArr.length; i += 1) { readDir(Path.join(src, Util.manifestResDir, checkArr[i]), manifest.assets, ""); } readDir(Path.join(src, 'src'), manifest.assets, ""); writeToManifest(manifest, '', ''); this._packageVersion2(bundlePath, checkArr); }, _packageDir(rootPath, zip) { let dir = Fs.readdirSync(rootPath); for (let i = 0; i < dir.length; i++) { let itemDir = dir[i]; let itemFullPath = Path.join(rootPath, itemDir); let stat = Fs.statSync(itemFullPath); if (stat.isFile()) { zip.file(itemDir, Fs.readFileSync(itemFullPath)); } else if (stat.isDirectory()) { this._packageDir(itemFullPath, zip.folder(itemDir)); } } }, _packageVersion2(bundlePath, checkArr) { let { manifestResDir } = Util; let JSZip = Editor.require( 'packages://hot-update-tools/node_modules/jszip', ); this.log('[Pack] 开始打包版本 ...'); let packageManifest = (zip, bundleName, underline) => { // 打包manifest文件 let manifestName = bundleName + underline; let path = Path.join(this.genManifestDir, bundleName); let version = Path.join(path, manifestName + 'version.manifest'); let project = Path.join(path, manifestName + 'project.manifest'); zip.file(manifestName + 'project.manifest', Fs.readFileSync(project)); zip.file(manifestName + 'version.manifest', Fs.readFileSync(version)); return version; } let packageSources = (zip, sources, local) => { // 要打包的资源 let resPath = Path.join(this.resourceRootDir, manifestResDir, sources); this._packageDir(resPath, zip.folder(manifestResDir + local)); } let packageZips = (zip, bundle, version) => { // 打包的文件名 let versionData = Fs.readFileSync(version, 'utf-8'); let versionJson = JSON.parse(versionData); let versionStr = versionJson.version; // 版本 // 打包到目录,生成zip versionStr = versionStr.replace('.', '_'); let zipName = 'ver_' + versionStr + '.zip'; let zipDir = Path.join(OutPut.versionsDir, bundle); let zipFilePath = Path.join(zipDir, zipName); if (!Fs.existsSync(zipDir)) { FsExtra.ensureDirSync(zipDir); } if (Fs.existsSync(zipFilePath)) { // 存在该版本的zip Fs.unlinkSync(zipFilePath); this.log('[Pack] 发现该版本的zip, 已经删除!'); } else { } zip .generateNodeStream({ type: 'nodebuffer', streamFiles: true, }) .pipe(Fs.createWriteStream(zipFilePath)) .on('finish', () => { this.log('[Pack] 打包成功: ' + zipFilePath); }) .on('error', (event) => { this.log('[Pack] 打包失败:' + event.message); }); } //bundle打包 for (let i = 0; i < bundlePath.length; i += 1) { let zip = new JSZip(); let bundle = bundlePath[i]; let version = packageManifest(zip, bundle, "_"); packageSources(zip, bundle, ''); packageZips(zip, bundle, version); } //assets src打包 let zip = new JSZip(); let version = packageManifest(zip, '', ''); let srcPath = Path.join(this.resourceRootDir, 'src'); this._packageDir(srcPath, zip.folder('src')); for (let i = 0; i < checkArr.length; i += 1) { let project = checkArr[i]; packageSources(zip, project, "/" + project); } packageZips(zip, '', version); }, _checkResourceRootDir(jsbDir) { if (Fs.existsSync(jsbDir)) { let srcPath = Path.join(jsbDir, 'src'); if (!Fs.existsSync(srcPath)) { this.log(`没有发现 ${srcPath}, 请先构建项目.`); return false; } let resArray = ['res', 'assets']; for (let i = 0; i < resArray.length; i++) { let item = resArray[i]; if (Fs.existsSync(Path.join(jsbDir, item))) { Util.manifestResDir = item; break; } } if (!Util.manifestResDir) { this.log(`没有发现资源目录${resArray.toString()}, 请先构建项目.`); return false; } this.resourceRootDir = jsbDir; this._saveConfig(); return true; } else { this.log(`没有发现 ${jsbDir}, 请先构建项目.`); return false; } }, changeSubPackDisabled(index) { Vue.set(this.subPackArr[index], "disabled", !this.subPackArr[index].disabled); this._saveConfig(); }, onInSubPackName(index) { this.oldSubPackName = index; }, onLeaveSubPackName(index) { if (this.oldSubPackName == index) { return; } this.subPackArr[this.oldSubPackName].subPackUrl = this.serverRootDir + '/' + index; Vue.set(this.subPackArr, index, this.subPackArr[this.oldSubPackName]); Vue.delete(this.subPackArr, [this.oldSubPackName]); this._saveConfig(); this._getServerVersion(index, (version) => { this.subPackArr[index].versionNumber = version; Vue.set(this.subPackArr, index, this.subPackArr[index]); this._saveConfig(); }) }, addSubPack() { if (this.subPackArr["命名子包"]) { this.log('请先填写之前创建的子包信息'); } else { Vue.set(this.subPackArr, "命名子包", { versionNumber: 0, disabled: false, subPackUrl: "请先输入子包名" }) this._saveConfig(); } }, delSubPack(index) { Vue.delete(this.subPackArr, index); this._saveConfig(); }, onMoveJSBLinkGameRes() { let data = CfgUtil._getConfigData(); this.log(JSON.stringify(data)) if(data) { let subPackArr = data.subPackArr; let projectDir = data.resourceRootDir; Editor.log(`项目路径:${projectDir}`) let keys = Object.keys(subPackArr); Editor.log(keys) for(let i = 0; i < keys.length; ++i) { const name = keys[i]; const element = subPackArr[name]; Editor.log(name) Editor.log(JSON.stringify(element)) if(element) { let destDir = Path.join(projectDir, 'assets\\'+name); Editor.log(destDir) if(Fs.existsSync(destDir)) { Editor.log(`${name} 存在`); let assetsDir = Path.join(destDir, 'assets'); if(!Fs.existsSync(assetsDir)) { Fs.mkdirSync(assetsDir); } FsExtra.emptyDirSync(assetsDir); let impDir = Path.join(assetsDir, 'import'); let natDir = Path.join(assetsDir, 'import'); if(!Fs.existsSync(impDir)) { Fs.mkdirSync(impDir); } if(!Fs.existsSync(natDir)) { Fs.mkdirSync(natDir); } FsExtra.copySync(impDir, Path.join(destDir, 'import')); FsExtra.move(natDir, Path.join(destDir, 'native')); FsExtra.copyFileSync(Path.join(assetsDir, 'config.json'), Path.join(destDir, 'config.json')); }else{ Editor.log(`${name} 不存在`); } } } } }, onCopyFileToReduceDir() { GoogleAnalytics.eventCustom('onCopyFileToReduceDir'); let { manifestResDir } = Util; let { manifestDir,reduceDir,versionsDir } = OutPut; let { resourceRootDir,serverRootDir } = CfgUtil.cfgData; // 检查资源目录 let srcPath = Path.join(resourceRootDir, 'src'); let resPath = Path.join(resourceRootDir, manifestResDir); if(!Fs.existsSync(resourceRootDir)) { this.log('资源目录不存在:'+resourceRootDir+',请构建项目'); return; } if (!Fs.existsSync(srcPath)) { this.log(resourceRootDir + '不存在src目录, 无法拷贝文件'); return; } if (!Fs.existsSync(resPath)) { this.log(resourceRootDir + '不存在res目录, 无法拷贝文件'); return; } this.log('清空文件夹'); this.delFile(reduceDir, reduceDir); // let versionUrl = this.serverRootDir + '/project.manifest'; let versionUrl = this.serverRootDir + '/assets/project.manifest'; let newUrl = manifestDir + '/project.manifest'; let xhr = new XMLHttpRequest(); xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 400) { let text = xhr.responseText; let json = null; try { json = JSON.parse(text); } catch (e) { console.log(e); this.log('获取远程版本号失败!'); return; } this.log(json.version); if(Fs.existsSync(newUrl)) { // this.log('新manifest存在'); Fs.readFile(newUrl, 'utf-8', (err, data)=>{ if(!err) { // this.log('有数据'); let newAssets = JSON.parse(data.toString()).assets; let oldAssets = json.assets; // this.log(newAssets); // this.log(oldAssets); let dataArray = []; for (var newKey in newAssets) { let newValue = newAssets[newKey]; let newElementSize = newValue["size"]; let newElementMd5 = newValue["md5"]; let isFind = false; for (var oldKey in oldAssets) { let oldValue = oldAssets[oldKey]; let oldElementSize = oldValue["size"]; let oldElementMd5 = oldValue["md5"]; if (oldKey == newKey && oldElementSize == newElementSize && oldElementMd5 == newElementMd5) { isFind = true; break; } } if (!isFind) {//没找到 //cb("找到差异文件:" + newKey + " size:" + newElementSize + " md5:" + newElementMd5); dataArray.push(newKey); } } this.log('筛选成功,共'+dataArray.length+'个文件'); for(let i = 0; i < dataArray.length; ++i) { let srcPath = Editor.Project.path + "\\build\\jsb-link\\" + dataArray[i]; let destPath = reduceDir + "\\" + dataArray[i]; this.copyFile(srcPath, destPath); } this.copyFile(manifestDir+'\\project.manifest', reduceDir+'\\project.manifest'); this.copyFile(manifestDir+'\\version.manifest', reduceDir+'\\version.manifest'); this.log('热更新资源检索完毕') } }) }else{ this.log('本地manifest不存在,请生成'); } } else if (xhr.status === 404 || xhr.status === 0) { // this.log("远端地址错误"); } }; xhr.open('get', versionUrl, true); xhr.setRequestHeader('If-Modified-Since', '0'); // 不缓存 xhr.send(); }, delFile(path, reservePath) { if(Fs.existsSync(path)) { if(Fs.statSync(path).isDirectory()) { let files = Fs.readdirSync(path); files.forEach((file, index) => { let currentPath = path + '/' + file; if(Fs.statSync(currentPath).isDirectory()) { this.delFile(currentPath, reservePath); }else{ Fs.unlinkSync(currentPath); } }); if(path != reservePath) { Fs.rmdirSync(path); } }else{ Fs.unlinkSync(path); } } }, copyFile(srcPath, destPath) { this.mkdirsSync(Path.dirname(destPath)); Fs.writeFileSync(destPath, Fs.readFileSync(srcPath)); }, mkdirsSync(dirname) { if(Fs.existsSync(dirname)) { return true; }else{ if(this.mkdirsSync(Path.dirname(dirname))) { Fs.mkdirSync(dirname); return true; } } }, }, });