feat: 提交资源

This commit is contained in:
han_han9
2025-10-28 21:55:41 +08:00
parent 591f398085
commit 55c4fcd9ae
2146 changed files with 172747 additions and 456 deletions

View File

@@ -0,0 +1,157 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { BuildPlugin } from "@cocos/creator-types/editor/packages/builder/@types/public";
const options = {
hotupdatePluginCheck: {
label: 'i18n:max-framework.hotupdate.hotupdatecheck',
description: "hotupdate checkbox",
default: false,
render: {
ui: 'ui-checkbox',
attributes: {
}
}
},
configInput: {
label: "i18n:max-framework.hotupdate.hotupdatecfg",
description: "hotupdate config file path",
extensions: ['json'],
default: 'project://hotupdate-config.json',
render: {
ui: 'ui-file',
attributes: {
protocols: 'project',
value: 'project://hotupdate-config.json',
}
},
},
outInput: {
label: "i18n:max-framework.hotupdate.outputcfg",
description: "hotupdate output directory",
default: 'project://hotupdate-packages',
render: {
ui: 'ui-file',
attributes: {
protocols: 'project',
type: 'directory',
value: 'project://hotupdate-packages',
}
},
},
// MinIO 配置选项
minioAutoUpload: {
label: 'i18n:max-framework.hotupdate.autoupload',
description: "Enable automatic upload to MinIO after build",
default: false,
render: {
ui: 'ui-checkbox',
attributes: {}
}
},
minioEndpoint: {
label: "i18n:max-framework.hotupdate.endpoint",
description: "MinIO server endpoint",
default: '',
displayCondition: {
minioAutoUpload: true
},
render: {
ui: 'ui-input',
attributes: {
placeholder: 'example: minio.example.com or 192.168.1.100:9000'
}
},
},
minioAccessKeyId: {
label: "i18n:max-framework.hotupdate.accesskey",
description: "MinIO access key ID",
default: 'Wh32ZQrSq742n119Ta9m',
displayCondition: {
minioAutoUpload: true
},
render: {
ui: 'ui-input',
attributes: {
placeholder: 'Access Key ID'
}
},
},
minioSecretAccessKey: {
label: "i18n:max-framework.hotupdate.secretkey",
description: "MinIO secret access key",
default: 'OCvFbPC9wIzkP7hWywFEn3BhPiNoLjoOWKGZnAn0',
displayCondition: {
minioAutoUpload: true
},
render: {
ui: 'ui-input',
attributes: {
placeholder: 'Secret Access Key',
type: 'password'
}
},
},
minioBucketName: {
label: "i18n:max-framework.hotupdate.bucket",
description: "MinIO bucket name",
default: 'hotupdate',
displayCondition: {
minioAutoUpload: true
},
render: {
ui: 'ui-input',
attributes: {
placeholder: 'Bucket name'
}
},
},
minioUseSSL: {
label: 'i18n:max-framework.hotupdate.usessl',
description: "Use SSL for MinIO connection",
default: false,
displayCondition: {
minioAutoUpload: true
},
render: {
ui: 'ui-checkbox',
attributes: {}
}
},
minioPathPrefix: {
label: "i18n:max-framework.hotupdate.pathprefix",
description: "MinIO path prefix",
default: 'hotupdate',
displayCondition: {
minioAutoUpload: true
},
render: {
ui: 'ui-input',
attributes: {
placeholder: 'Path prefix (optional)'
}
},
},
};
export const configs: BuildPlugin.Configs = {
'*': {
hooks: './hooks',
},
"mac": {
options: options,
},
"ios": {
options: options,
},
"android": {
options: options,
},
"windows": {
options: options,
},
"web-mobile": {
panel: "./panel"
}
};

View File

@@ -0,0 +1,38 @@
import { existsSync, readFileSync, writeFileSync } from "fs";
import { ensureFileSync } from "fs-extra";
import path from "path";
export class CfgData {
version: string = "1.0.0";
md5: string = "";
}
export class CfgUtils {
private static getCfgFile(pkg: string) {
if (!pkg || pkg.trim() === '') {
throw new Error('包名不能为空');
}
return path.join(__dirname, `../../../temp/max-framework-hotupdate/${pkg}.json`);
}
static getCfgData(pkg: string): CfgData {
const cfgFile = CfgUtils.getCfgFile(pkg);
if (existsSync(cfgFile)) {
const data = readFileSync(cfgFile, 'utf-8');
const cfgData = JSON.parse(data) as CfgData;
console.log('读取配置文件', pkg, JSON.stringify(cfgData));
return cfgData;
} else {
const data = new CfgData();
CfgUtils.savaConfig(pkg, data);
return data;
}
}
static savaConfig(pkg: string, data: CfgData) {
console.log('保存配置文件', pkg, JSON.stringify(data));
const cfgFile = CfgUtils.getCfgFile(pkg);
ensureFileSync(cfgFile);
writeFileSync(cfgFile, JSON.stringify(data), 'utf-8');
}
}

View File

@@ -0,0 +1,31 @@
import { generator, GenPackageConfig } from "./version_generator";
let configs:GenPackageConfig[] = []
let assetRootDirPath:string = ''
let outputPath: string = '';
let i = 2;
while (i < process.argv.length) {
let arg = process.argv[i];
switch(arg) {
case '-configPath':
configs = require(process.argv[i + 1]) as GenPackageConfig[];
break;
case '-assetRootDirPath':
assetRootDirPath = process.argv[i + 1];
break;
case '-outputDirPath':
outputPath = process.argv[i + 1];
break;
default:
console.error('unknown arg:', arg);
break;
}
i += 2
}
for (let item of configs) {
let src = assetRootDirPath;
generator(src, outputPath, item);
}

View File

@@ -0,0 +1,227 @@
import {
BuildHook,
IBuildResult,
IBuildTaskOption,
} from "@cocos/creator-types/editor/packages/builder/@types/public";
import path from "path";
import { existsSync, readFile, writeFile } from "fs";
import { ensureDirSync } from "fs-extra";
import { generator, GenPackageConfig } from "./version_generator";
import { createMinIOUploader, MinIOConfig } from "./minio-uploader";
const injectScript = `(function () {
const native = globalThis?.jsb || window.jsb || native;
if (typeof native === 'object') {
var hotUpdateSearchPaths = localStorage.getItem('HotUpdateSearchPaths');
if (hotUpdateSearchPaths) {
var paths = JSON.parse(hotUpdateSearchPaths);
native.fileUtils.setSearchPaths(paths);
var fileList = [];
var storagePath = paths[0] || '';
var tempPath = storagePath + '_temp/';
var baseOffset = tempPath.length;
if (native.fileUtils.isDirectoryExist(tempPath) && !native.fileUtils.isFileExist(tempPath + 'project.manifest.temp')) {
native.fileUtils.listFilesRecursively(tempPath, fileList);
fileList.forEach(srcPath => {
var relativePath = srcPath.substr(baseOffset);
var dstPath = storagePath + relativePath;
if (srcPath[srcPath.length - 1] === '/') {
native.fileUtils.createDirectory(dstPath)
}
else {
if (native.fileUtils.isFileExist(dstPath)) {
native.fileUtils.removeFile(dstPath)
}
native.fileUtils.renameFile(srcPath, dstPath);
}
})
native.fileUtils.removeDirectory(tempPath);
}
}
}
})();`;
export const onBeforeBuild: BuildHook.onBeforeBuild = async function (
options: IBuildTaskOption,
result: IBuildResult
) {};
export const onAfterBuild: BuildHook.onAfterBuild = async function (
options: IBuildTaskOption,
result: IBuildResult
) {
console.log("onAfterBuild platform", options.platform);
if (!["android", "mac", "ios", "windows"].includes(options.platform)) {
return;
}
let url = path.join(result.dest, "data", "main.js");
console.log("onAfterBuild url", url);
if (!existsSync(url)) {
url = path.join(result.dest, "assets", "main.js");
}
console.log("onAfterBuild url", url);
const hotupdateConfig: {
hotupdatePluginCheck: boolean;
configInput: string;
outInput: string;
// MinIO 配置
minioAutoUpload: boolean;
minioEndpoint: string;
minioAccessKeyId: string;
minioSecretAccessKey: string;
minioBucketName: string;
minioUseSSL: boolean;
minioPathPrefix: string;
} = options.packages?.["max-framework"];
console.log(
"onAfterBuild hotupdateConfig",
url,
hotupdateConfig.hotupdatePluginCheck,
hotupdateConfig.configInput,
hotupdateConfig.outInput,
JSON.stringify(hotupdateConfig)
);
if (!!hotupdateConfig.hotupdatePluginCheck) {
// 验证配置参数
if (!hotupdateConfig.configInput || !hotupdateConfig.outInput) {
console.error("热更新配置不完整configInput 和 outInput 不能为空");
return;
}
try {
// configInput 是带 project:// 前缀的,需要转换为实际文件路径
let configPath = hotupdateConfig.configInput;
if (configPath.startsWith("project://")) {
configPath = path.resolve(
Editor.Project.path,
configPath.replace("project://", "")
);
} else if (!path.isAbsolute(configPath)) {
configPath = path.resolve(Editor.Project.path, configPath);
}
let outInput = hotupdateConfig.outInput;
if (outInput.startsWith("project://")) {
outInput = path.resolve(
Editor.Project.path,
outInput.replace("project://", "")
);
} else if (!path.isAbsolute(outInput)) {
outInput = path.resolve(Editor.Project.path, outInput);
}
console.log("onAfterBuild configPath", configPath, outInput);
const obj = require(configPath) as GenPackageConfig[];
for (const item of obj) {
// 验证包名不能为空
if (!item.pkgName || item.pkgName.trim() === "") {
console.error("热更新包名不能为空:", item);
continue;
}
const source = path.join(result.dest, "data");
console.log("开始生成Package: ", item.pkgName, source);
ensureDirSync(
path.join(outInput, item.pkgName)
);
generator(source, outInput, item);
}
// MinIO上传功能
if (hotupdateConfig.minioAutoUpload) {
console.log("开始MinIO自动上传流程...");
await uploadToMinIO(hotupdateConfig, outInput, obj);
}
} catch (error) {
console.error("热更新配置文件加载失败:", error);
return;
}
}
return new Promise((resolve, reject) => {
readFile(url, "utf8", (err, data) => {
if (err) {
reject(err);
return;
}
const newStr = injectScript + data;
writeFile(url, newStr, (err) => {
if (err) {
reject(err);
return;
}
resolve();
});
});
});
};
/**
* 上传到MinIO
* @param config 热更新配置
* @param outputPath 输出路径
* @param packages 包配置列表
*/
async function uploadToMinIO(
config: {
minioEndpoint: string;
minioAccessKeyId: string;
minioSecretAccessKey: string;
minioBucketName: string;
minioUseSSL: boolean;
minioPathPrefix: string;
},
outputPath: string,
packages: GenPackageConfig[]
) {
try {
// 验证MinIO配置
if (!config.minioEndpoint || !config.minioAccessKeyId ||
!config.minioSecretAccessKey || !config.minioBucketName) {
console.error("MinIO配置不完整跳过上传");
return;
}
// 创建MinIO上传器
const minioConfig: MinIOConfig = {
endpoint: config.minioEndpoint,
accessKeyId: config.minioAccessKeyId,
secretAccessKey: config.minioSecretAccessKey,
bucketName: config.minioBucketName,
useSSL: config.minioUseSSL,
pathPrefix: config.minioPathPrefix
};
const uploader = createMinIOUploader(minioConfig);
// 上传每个包
for (const pkg of packages) {
if (!pkg.pkgName || pkg.pkgName.trim() === "") {
continue;
}
const packageDir = path.join(outputPath, pkg.pkgName);
if (!existsSync(packageDir)) {
console.error(`包目录不存在: ${packageDir}`);
continue;
}
console.log(`开始上传包到MinIO: ${pkg.pkgName}`);
const result = await uploader.uploadDirectory(packageDir, pkg.pkgName);
if (result.success) {
console.log(`${pkg.pkgName} 上传成功,共 ${result.uploadedFiles.length} 个文件`);
console.log(`上传的文件: ${result.uploadedFiles.join(', ')}`);
} else {
console.error(`${pkg.pkgName} 上传失败: ${result.error}`);
}
}
console.log("所有包MinIO上传完成");
} catch (error) {
console.error("MinIO上传过程中发生错误:", error);
}
}

View File

@@ -0,0 +1,229 @@
import { createHash } from "crypto";
import fs from "fs";
import path from "path";
/**
* MinIO上传配置接口
*/
export interface MinIOConfig {
/** MinIO服务器地址 */
endpoint: string;
/** 访问密钥ID */
accessKeyId: string;
/** 访问密钥Secret */
secretAccessKey: string;
/** 存储桶名称 */
bucketName: string;
/** 是否使用SSL */
useSSL: boolean;
/** 上传路径前缀 */
pathPrefix?: string;
}
/**
* 上传结果接口
*/
export interface UploadResult {
/** 是否成功 */
success: boolean;
/** 错误信息 */
error?: string;
/** 上传的文件列表 */
uploadedFiles: string[];
}
/**
* MinIO上传器类
*/
export class MinIOUploader {
private config: MinIOConfig;
constructor(config: MinIOConfig) {
this.config = config;
}
/**
* 验证MinIO配置
*/
private validateConfig(): boolean {
const required = ['endpoint', 'accessKeyId', 'secretAccessKey', 'bucketName'];
for (const field of required) {
if (!this.config[field as keyof MinIOConfig] ||
String(this.config[field as keyof MinIOConfig]).trim() === '') {
console.error(`MinIO配置错误: ${field} 不能为空`);
return false;
}
}
return true;
}
/**
* 上传目录到MinIO
* @param localDir 本地目录路径
* @param remotePrefix 远程路径前缀
* @returns 上传结果
*/
async uploadDirectory(localDir: string, remotePrefix: string = ''): Promise<UploadResult> {
const result: UploadResult = {
success: false,
uploadedFiles: []
};
try {
// 验证配置
if (!this.validateConfig()) {
result.error = 'MinIO配置验证失败';
return result;
}
// 验证本地目录是否存在
if (!fs.existsSync(localDir)) {
result.error = `本地目录不存在: ${localDir}`;
return result;
}
console.log(`开始上传目录到MinIO: ${localDir} -> ${remotePrefix}`);
// 动态导入minio模块
const MinIO = await this.loadMinIOModule();
if (!MinIO) {
result.error = 'MinIO模块加载失败请确保已安装minio依赖';
return result;
}
// 创建MinIO客户端
const minioClient = new MinIO.Client({
endPoint: this.config.endpoint.replace(/^https?:\/\//, ''),
port: this.getPortFromEndpoint(),
useSSL: this.config.useSSL,
accessKey: this.config.accessKeyId,
secretKey: this.config.secretAccessKey
});
// 检查存储桶是否存在
const bucketExists = await minioClient.bucketExists(this.config.bucketName);
if (!bucketExists) {
console.log(`存储桶不存在,尝试创建: ${this.config.bucketName}`);
await minioClient.makeBucket(this.config.bucketName);
}
// 获取所有文件
const files = this.getAllFiles(localDir);
console.log(`找到 ${files.length} 个文件需要上传`);
// 上传文件
for (const filePath of files) {
try {
const relativePath = path.relative(localDir, filePath);
const objectName = this.buildObjectName(remotePrefix, relativePath);
console.log(`上传文件: ${filePath} -> ${objectName}`);
await minioClient.fPutObject(this.config.bucketName, objectName, filePath);
result.uploadedFiles.push(objectName);
console.log(`文件上传成功: ${objectName}`);
} catch (fileError) {
console.error(`文件上传失败: ${filePath}`, fileError);
// 继续上传其他文件,不中断整个流程
}
}
result.success = result.uploadedFiles.length > 0;
if (!result.success) {
result.error = '没有文件上传成功';
}
console.log(`MinIO上传完成成功上传 ${result.uploadedFiles.length} 个文件`);
} catch (error) {
console.error('MinIO上传过程中发生错误:', error);
result.error = error instanceof Error ? error.message : String(error);
}
return result;
}
/**
* 动态加载MinIO模块
*/
private async loadMinIOModule(): Promise<any> {
try {
// 尝试动态导入minio模块
const minio = await import('minio');
return minio;
} catch (error) {
console.error('MinIO模块导入失败:', error);
console.log('请运行以下命令安装MinIO依赖:');
console.log('npm install minio');
return null;
}
}
/**
* 从endpoint中提取端口号
*/
private getPortFromEndpoint(): number | undefined {
try {
const url = new URL(this.config.endpoint.startsWith('http') ?
this.config.endpoint : `http://${this.config.endpoint}`);
return url.port ? parseInt(url.port) : (this.config.useSSL ? 443 : 80);
} catch {
return this.config.useSSL ? 443 : 80;
}
}
/**
* 构建对象名称
*/
private buildObjectName(remotePrefix: string, relativePath: string): string {
const prefix = this.config.pathPrefix || '';
const parts = [prefix, remotePrefix, relativePath].filter(part => part && part.trim() !== '');
return parts.join('/').replace(/\/+/g, '/');
}
/**
* 递归获取目录下的所有文件
*/
private getAllFiles(dir: string): string[] {
const files: string[] = [];
const traverse = (currentDir: string) => {
const items = fs.readdirSync(currentDir);
for (const item of items) {
// 跳过以点开头的隐藏文件和隐藏目录
if (item.startsWith('.')) {
continue;
}
const fullPath = path.join(currentDir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
traverse(fullPath);
} else if (stat.isFile()) {
files.push(fullPath);
}
}
};
traverse(dir);
return files;
}
/**
* 计算文件MD5
*/
private calculateFileMD5(filePath: string): string {
const content = fs.readFileSync(filePath);
return createHash('md5').update(content).digest('hex');
}
}
/**
* 创建MinIO上传器实例
*/
export function createMinIOUploader(config: MinIOConfig): MinIOUploader {
return new MinIOUploader(config);
}

View File

@@ -0,0 +1,226 @@
import { IBuildTaskOption } from "@cocos/creator-types/editor/packages/builder/@types";
export const style = `
.hotupdate-plugin {
padding: 10px;
}
.minio-section {
margin-top: 15px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.minio-title {
font-weight: bold;
margin-bottom: 10px;
color: #333;
}
.minio-config-row {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.minio-config-row ui-label {
width: 120px;
min-width: 120px;
}
.minio-config-row ui-input,
.minio-config-row ui-checkbox {
flex: 1;
margin-left: 10px;
}
.minio-test-button {
margin-top: 10px;
}
`;
export const temple = `
<div class="hotupdate-plugin">
<div class="minio-section">
<div class="minio-title">MinIO 上传配置</div>
<div class="minio-config-row">
<ui-label value="自动上传"></ui-label>
<ui-checkbox id="minio-auto-upload"></ui-checkbox>
</div>
<div class="minio-config-row">
<ui-label value="服务器地址"></ui-label>
<ui-input id="minio-endpoint" placeholder="例: minio.example.com:9000"></ui-input>
</div>
<div class="minio-config-row">
<ui-label value="Access Key"></ui-label>
<ui-input id="minio-access-key" placeholder="Access Key ID"></ui-input>
</div>
<div class="minio-config-row">
<ui-label value="Secret Key"></ui-label>
<ui-input id="minio-secret-key" type="password" placeholder="Secret Access Key"></ui-input>
</div>
<div class="minio-config-row">
<ui-label value="存储桶名称"></ui-label>
<ui-input id="minio-bucket" placeholder="hotupdate"></ui-input>
</div>
<div class="minio-config-row">
<ui-label value="使用SSL"></ui-label>
<ui-checkbox id="minio-use-ssl"></ui-checkbox>
</div>
<div class="minio-config-row">
<ui-label value="路径前缀"></ui-label>
<ui-input id="minio-path-prefix" placeholder="hotupdate"></ui-input>
</div>
<div class="minio-test-button">
<ui-button id="test-minio-connection">测试连接</ui-button>
</div>
</div>
</div>
`;
export const $ = {
root: '.hotupdate-plugin',
minioAutoUpload: '#minio-auto-upload',
minioEndpoint: '#minio-endpoint',
minioAccessKey: '#minio-access-key',
minioSecretKey: '#minio-secret-key',
minioBucket: '#minio-bucket',
minioUseSSL: '#minio-use-ssl',
minioPathPrefix: '#minio-path-prefix',
testButton: '#test-minio-connection',
}
export async function update(options: IBuildTaskOption, key: string) {
console.log(`update options ${options} and key ${key}`);
if (key) {
return;
}
updateMinIOConfigFromOptions(options);
}
export async function ready(options: IBuildTaskOption) {
console.log(`ready options ${options}`);
init();
updateMinIOConfigFromOptions(options);
}
export async function close() {
// 清理资源
}
function init() {
// 绑定测试连接按钮事件
const testButton = document.querySelector($.testButton) as HTMLElement;
if (testButton) {
testButton.addEventListener('click', testMinIOConnection);
}
// 绑定自动上传复选框事件
const autoUploadCheckbox = document.querySelector($.minioAutoUpload) as HTMLInputElement;
if (autoUploadCheckbox) {
autoUploadCheckbox.addEventListener('change', onAutoUploadChange);
}
}
/**
* 从构建选项中更新MinIO配置
*/
function updateMinIOConfigFromOptions(options: IBuildTaskOption) {
const config = options.packages?.['max-framework'];
if (!config) return;
// 更新UI元素的值
updateInputValue($.minioAutoUpload, config.minioAutoUpload);
updateInputValue($.minioEndpoint, config.minioEndpoint);
updateInputValue($.minioAccessKey, config.minioAccessKeyId);
updateInputValue($.minioSecretKey, config.minioSecretAccessKey);
updateInputValue($.minioBucket, config.minioBucketName);
updateInputValue($.minioUseSSL, config.minioUseSSL);
updateInputValue($.minioPathPrefix, config.minioPathPrefix);
}
/**
* 更新输入框的值
*/
function updateInputValue(selector: string, value: any) {
const element = document.querySelector(selector) as HTMLInputElement;
if (element) {
if (element.type === 'checkbox') {
element.checked = Boolean(value);
} else {
element.value = String(value || '');
}
}
}
/**
* 自动上传复选框变化事件
*/
function onAutoUploadChange(event: Event) {
const checkbox = event.target as HTMLInputElement;
const isEnabled = checkbox.checked;
// 可以在这里添加额外的逻辑,比如当启用自动上传时显示警告
if (isEnabled) {
console.log('已启用MinIO自动上传');
}
}
/**
* 测试MinIO连接
*/
async function testMinIOConnection() {
try {
// 获取当前配置
const config = {
endpoint: (document.querySelector($.minioEndpoint) as HTMLInputElement)?.value || '',
accessKeyId: (document.querySelector($.minioAccessKey) as HTMLInputElement)?.value || '',
secretAccessKey: (document.querySelector($.minioSecretKey) as HTMLInputElement)?.value || '',
bucketName: (document.querySelector($.minioBucket) as HTMLInputElement)?.value || '',
useSSL: (document.querySelector($.minioUseSSL) as HTMLInputElement)?.checked || false,
pathPrefix: (document.querySelector($.minioPathPrefix) as HTMLInputElement)?.value || ''
};
// 验证必填字段
if (!config.endpoint || !config.accessKeyId || !config.secretAccessKey || !config.bucketName) {
alert('请填写完整的MinIO配置信息');
return;
}
// 添加加载状态
const testButton = document.querySelector($.testButton) as HTMLElement;
const originalText = testButton.textContent;
testButton.textContent = '测试中...';
testButton.setAttribute('disabled', 'true');
// 这里可以添加实际的连接测试逻辑
// 由于在面板环境中,我们只做基本验证
await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟网络请求
alert('MinIO配置验证通过\n注意实际连接测试将在构建时进行。');
// 恢复按钮状态
testButton.textContent = originalText;
testButton.removeAttribute('disabled');
} catch (error) {
console.error('测试MinIO连接失败:', error);
alert(`测试失败: ${error}`);
// 恢复按钮状态
const testButton = document.querySelector($.testButton) as HTMLElement;
testButton.textContent = '测试连接';
testButton.removeAttribute('disabled');
}
}

View File

@@ -0,0 +1,184 @@
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}`);
}

View File

@@ -0,0 +1,30 @@
/* eslint-disable no-console */
/**
* @en Registration method for the main process of Extension
* @zh 为扩展的主进程的注册方法
*/
export const methods: { [key: string]: (...any: any) => any } = {
/**
* @en A method that can be triggered by message
* @zh 通过 message 触发的方法
*/
showLog() {
console.log('Max Framework - 集成热更新功能');
},
};
/**
* @en Method Triggered on Extension Startup
* @zh 扩展启动时触发的方法
*/
export function load() {
console.log('Max Framework 已加载,包含热更新功能');
}
/**
* @en Method triggered when uninstalling the extension
* @zh 卸载扩展时触发的方法
*/
export function unload() {
console.log('Max Framework 已卸载');
}

View File

@@ -0,0 +1 @@
{"packageUrl":"","remoteManifestUrl":"","remoteVersionUrl":"","version":"1.0.2","assets":{"assets/main/cc.config.json":{"size":1598,"md5":"227c50fbb3b95e0c0614f7059cf28981"},"assets/main/import/06/0631e4814.json":{"size":3934,"md5":"1d65abfd431ce3f2b48779550d5b34b2"},"assets/main/import/0a/0a6a6b986.json":{"size":162,"md5":"6e96a77d78ea68de295adbcd5e20a350"},"assets/main/import/0f/0fb493793.json":{"size":5285,"md5":"d09b20a4bf30d465318b4f999977b136"},"assets/main/import/20/20835ba4-6145-4fbc-a58a-051ce700aa3e.json":{"size":72,"md5":"a253314ec7107d95345d7fc89b8d7bb5"},"assets/main/import/54/544e49d6-3f05-4fa8-9a9e-091f98fc2ce8.json":{"size":72,"md5":"a253314ec7107d95345d7fc89b8d7bb5"},"assets/main/import/95/951249e0-9f16-456d-8b85-a6ca954da16b.json":{"size":72,"md5":"a253314ec7107d95345d7fc89b8d7bb5"},"assets/main/import/fd/fd8ec536-a354-4a17-9c74-4f3883c378c8.json":{"size":480,"md5":"731f44a39ce9377ee6de8ba34edaf039"},"assets/main/index.js":{"size":17734,"md5":"146ee7b427cabca827989a340b3e675b"},"assets/main/native/20/20835ba4-6145-4fbc-a58a-051ce700aa3e.png":{"size":1082,"md5":"90cf45d059d0408bec327f66eae5764c"},"assets/main/native/54/544e49d6-3f05-4fa8-9a9e-091f98fc2ce8.png":{"size":1114,"md5":"83fcc9912e01ae5411c357651fb8b1cf"},"assets/main/native/6f/6f01cf7f-81bf-4a7e-bd5d-0afc19696480@b47c0@40c10.png":{"size":54213,"md5":"6f2aff3dbb37bc738ef8702c01c4d4ee"},"assets/main/native/6f/6f01cf7f-81bf-4a7e-bd5d-0afc19696480@b47c0@74afd.png":{"size":54380,"md5":"459f6c02bda1ca98d67b78ff0522ff24"},"assets/main/native/6f/6f01cf7f-81bf-4a7e-bd5d-0afc19696480@b47c0@7d38f.png":{"size":6102,"md5":"6a9df37dc6828472be7f24fb3d00c4fb"},"assets/main/native/6f/6f01cf7f-81bf-4a7e-bd5d-0afc19696480@b47c0@8fd34.png":{"size":57592,"md5":"dabf1ceef338870610b5b1cda1373e6d"},"assets/main/native/6f/6f01cf7f-81bf-4a7e-bd5d-0afc19696480@b47c0@bb97f.png":{"size":60696,"md5":"e76a4d72b1717c81bcbe807e9aff00f1"},"assets/main/native/6f/6f01cf7f-81bf-4a7e-bd5d-0afc19696480@b47c0@e9a6d.png":{"size":48101,"md5":"9e6582188657cb9b81e78d242207f431"},"assets/main/native/95/951249e0-9f16-456d-8b85-a6ca954da16b.png":{"size":1050,"md5":"c06a93f5f1a8a1c6edc4fd8b52e96cbf"},"assets/main/native/d0/d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0@40c10.png":{"size":99071,"md5":"a9f261ee96213f104f5c6b2e8fdf458d"},"assets/main/native/d0/d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0@74afd.png":{"size":117874,"md5":"66393e5d36d7cafbdbf594eb3e8627fa"},"assets/main/native/d0/d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0@7d38f.png":{"size":35222,"md5":"a575781c30b0e25077099ab3a04e7e20"},"assets/main/native/d0/d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0@8fd34.png":{"size":103826,"md5":"2b5b19b4eea08e86a63bc65ebbb15a74"},"assets/main/native/d0/d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0@bb97f.png":{"size":117218,"md5":"8000e42dfcf81024daa91e8b2daa26ef"},"assets/main/native/d0/d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0@e9a6d.png":{"size":116066,"md5":"3645ba19643145acfff901b3e12137eb"},"assets/internal/cc.config.json":{"size":3122,"md5":"90ceacb6f83a3ae72460fe7b71a60223"},"assets/internal/import/0b/0b2bbb7a0.json":{"size":672639,"md5":"2a87cb2a91a8e8572e21413c7f5654b9"},"assets/internal/index.js":{"size":602,"md5":"cfec173a654f557700a224a8c94adb40"}},"searchPaths":[]}

View File

@@ -0,0 +1 @@
{"packageUrl":"","remoteManifestUrl":"","remoteVersionUrl":"","version":"1.0.2"}