@playcraft/devkit 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/cli/bin/playable-scripts.js +2 -0
  3. package/cli/commands/build.js +1 -0
  4. package/cli/commands/builds.js +16 -0
  5. package/cli/commands/dev.js +1 -0
  6. package/core/batch-build.js +149 -0
  7. package/core/cos-uploader.js +42 -0
  8. package/core/index.d.ts +167 -0
  9. package/core/index.js +1 -0
  10. package/core/loaders/gltf-loader.js +45 -0
  11. package/core/options.js +25 -0
  12. package/core/plugins/AdikteevInjectorPlugin.js +47 -0
  13. package/core/plugins/BigoAdsInjectorPlugin.js +7 -0
  14. package/core/plugins/DAPIInjectorPlugin.js +5 -0
  15. package/core/plugins/DebuggerInjectionPlugin.js +6 -0
  16. package/core/plugins/ExitAPIInjectorPlugin.js +10 -0
  17. package/core/plugins/FflateCompressionPlugin.js +225 -0
  18. package/core/plugins/LiftoffInjectorPlugin.js +14 -0
  19. package/core/plugins/MRAIDInjectorPlugin.js +4 -0
  20. package/core/plugins/MintegralInjectorPlugin.js +5 -0
  21. package/core/plugins/PangleInjectorPlugin.js +4 -0
  22. package/core/plugins/SnapchatInjectorPlugin.js +20 -0
  23. package/core/plugins/TikTokInjectorPlugin.js +4 -0
  24. package/core/plugins/UnityInjectorPlugin.js +20 -0
  25. package/core/plugins/ZipPlugin.js +19 -0
  26. package/core/resources/bigoads-config.json +1 -0
  27. package/core/resources/snapchat-config.json +1 -0
  28. package/core/resources/tiktok-config.json +1 -0
  29. package/core/utils/buildDefines.js +3 -0
  30. package/core/utils/buildTemplateString.js +6 -0
  31. package/core/utils/date.js +1 -0
  32. package/core/utils/generateAdikteevHtmlWebpackPluginConfig.js +36 -0
  33. package/core/utils/generateBigabidHtmlWebpackPluginConfig.js +13 -0
  34. package/core/utils/generateInMobiHtmlWebpackPluginConfig.js +36 -0
  35. package/core/utils/injectSDKPlugins.js +19 -0
  36. package/core/utils/logOptions.js +5 -0
  37. package/core/utils/mergeOptions.js +6 -0
  38. package/core/utils/parseArgvOptions.js +7 -0
  39. package/core/webpack.build.js +60 -0
  40. package/core/webpack.common.js +10 -0
  41. package/core/webpack.dev.js +15 -0
  42. package/defines.d.ts +54 -0
  43. package/package.json +79 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Smoud
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ "use strict";process.on("unhandledRejection",err=>{throw err});const spawn=require("cross-spawn");const args=process.argv.slice(2);const scriptIndex=args.findIndex(x=>x==="build"||x==="dev"||x==="builds");const script=scriptIndex===-1?args[0]:args[scriptIndex];const nodeArgs=scriptIndex>0?args.slice(0,scriptIndex):[];if(["build","dev","builds"].includes(script)){const result=spawn.sync(process.execPath,nodeArgs.concat(require.resolve("../commands/"+script)).concat(args.slice(scriptIndex+1)),{stdio:"inherit"});if(result.signal){if(result.signal==="SIGKILL"){console.log("The build failed because the process exited too early. "+"This probably means the system ran out of memory or someone called "+"`kill -9` on the process.")}else if(result.signal==="SIGTERM"){console.log("The build failed because the process exited too early. "+"Someone might have called `kill` or `killall`, or the system could "+"be shutting down.")}process.exit(1)}process.exit(result.status)}else{console.log("Unknown script \""+script+"\".")}
@@ -0,0 +1 @@
1
+ "use strict";process.env.BABEL_ENV="production";process.env.NODE_ENV="production";const{runBuild}=require("../../core/webpack.build.js");runBuild();
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ "use strict";/**
3
+ * Batch Builds Command for playable-scripts
4
+ *
5
+ * 支持:
6
+ * - 单个主题下多个渠道的并发构建
7
+ * - 独立临时目录,避免并发冲突
8
+ * - 灵活的配置系统
9
+ *
10
+ * 使用方法:
11
+ * playable-scripts builds # 使用默认配置
12
+ * playable-scripts builds -t "theme1,theme2" # 指定主题
13
+ * playable-scripts builds -c "google,facebook" # 指定渠道
14
+ * playable-scripts builds -t "theme1" -c "google" # 指定主题和渠道
15
+ * playable-scripts builds --parallel 3 # 设置并发数
16
+ */process.on("unhandledRejection",err=>{throw err});const{buildBatch}=require("../../core/batch-build");buildBatch();
@@ -0,0 +1 @@
1
+ "use strict";process.env.BABEL_ENV="development";process.env.NODE_ENV="development";const{runDev}=require("../../core/webpack.dev.js");runDev();
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ "use strict";/**
3
+ * 批量构建核心模块
4
+ *
5
+ * 核心特性:
6
+ * 1. 支持单个主题下多个渠道的并发构建
7
+ * 2. 每个构建任务使用独立的临时目录(dist-temp-{taskId})
8
+ * 3. 构建完成后自动移动到目标目录并清理临时目录
9
+ * 4. 支持灵活的配置系统
10
+ */const{execSync,exec}=require("child_process");const fs=require("fs");const path=require("path");const os=require("os");const{promisify}=require("util");const{uploadDirectoryToCOS}=require("./cos-uploader");const execAsync=promisify(exec);// ==================== 默认配置 ====================
11
+ /**
12
+ * 计算默认并发数
13
+ * 策略: CPU 核心数 - 1,最小为 2,最大为 8
14
+ * 例如:
15
+ * - 4 核 CPU → 3 并发
16
+ * - 8 核 CPU → 7 并发
17
+ * - 16 核 CPU → 8 并发(限制最大值)
18
+ */function getDefaultParallel(){const cpuCount=os.cpus().length;const calculated=Math.max(2,cpuCount-1);// 至少 2 个并发
19
+ const parallel=Math.min(8,calculated);// 最多 8 个并发
20
+ return parallel}const DEFAULT_CONFIG={title:"Phaser Game",appName:"",// 用于从远程仓库拉取配置
21
+ channels:["google","facebook","tiktok","applovin","ironSource","unity","moloco","liftoff","bigoads"],themes:{enabled:true,sourceDir:"src/theme",entryFile:"src/theme/index.ts",whitelist:null},naming:{language:"EN",format:"\u65E0",supplier:"AIX",follower:"ZQL"},build:{outputDir:"dist-all",tempDirPrefix:"dist-temp",continueOnError:true,cleanTemp:true,parallel:getDefaultParallel()// 根据 CPU 核心数自动设置
22
+ },// COS 配置(可选)
23
+ cosConfig:null// 如果配置,则自动为每个主题生成 preview 渠道并上传
24
+ };// ==================== 工具函数 ====================
25
+ let isExiting=false;let backupContent=null;let backupPath=null;const activeTempDirs=new Set;// 跟踪所有活动的临时目录
26
+ function log(message,level="info"){const timestamp=new Date().toLocaleTimeString();const colors={info:"\x1B[36m",success:"\x1B[32m",error:"\x1B[31m",warn:"\x1B[33m",debug:"\x1B[90m"};const reset="\x1B[0m";const prefix={info:"\uD83D\uDCE6",success:"\u2705",error:"\u274C",warn:"\u26A0\uFE0F",debug:"\uD83D\uDD0D"}[level]||"\u2139\uFE0F";console.log(`${colors[level]}[${timestamp}] ${prefix} ${message}${reset}`)}function getCurrentMonth(){const month=new Date().getMonth()+1;return`${month}月`}function getCurrentDate(){const date=new Date;const year=date.getFullYear();const month=String(date.getMonth()+1).padStart(2,"0");const day=String(date.getDate()).padStart(2,"0");return`${year}${month}${day}`}function defaultFileNameGenerator(config,themeName,channel){const parts=[themeName,config.naming.language,config.naming.format,getCurrentMonth(),getCurrentDate(),config.naming.supplier,config.naming.follower,channel.toLowerCase()].filter(item=>!!item);return parts.join("-")}function getAllThemes(config,projectRoot){const themesDir=path.join(projectRoot,config.themes.sourceDir);if(!fs.existsSync(themesDir)){log(`主题目录不存在: ${themesDir}`,"warn");return[]}const entries=fs.readdirSync(themesDir,{withFileTypes:true});let themes=entries.filter(entry=>entry.isDirectory()).map(entry=>entry.name).sort();if(config.themes.whitelist&&typeof config.themes.whitelist==="function"){themes=themes.filter(config.themes.whitelist);log(`应用主题白名单过滤: ${themes.length} 个主题`,"debug")}return themes}function backupEntryFile(config,projectRoot){const entryPath=path.join(projectRoot,config.themes.entryFile);if(!fs.existsSync(entryPath)){throw new Error(`入口文件不存在: ${entryPath}`)}backupPath=entryPath;backupContent=fs.readFileSync(entryPath,"utf8");log(`已备份入口文件: ${config.themes.entryFile}`,"debug")}function restoreEntryFile(){if(backupPath&&backupContent){fs.writeFileSync(backupPath,backupContent,"utf8");log(`已恢复入口文件`,"debug");backupPath=null;backupContent=null}}function switchTheme(config,themeName,projectRoot){const entryPath=path.join(projectRoot,config.themes.entryFile);// 使用配置文件中的 generateEntryFile 函数(如果提供)
27
+ let content;if(config.themes.switching&&typeof config.themes.switching.generateEntryFile==="function"){content=config.themes.switching.generateEntryFile(themeName)}else{// 兼容旧格式:简化的 export 语句
28
+ const themePath=`./${themeName}`;content=`export { default } from '${themePath}';\n`}fs.writeFileSync(entryPath,content,"utf8");log(`已切换主题: ${themeName}`,"debug")}/**
29
+ * 生成唯一的任务 ID
30
+ */function generateTaskId(){return`${Date.now()}-${Math.random().toString(36).substr(2,9)}`}/**
31
+ * 创建独立的临时目录
32
+ */function createTempDir(config,projectRoot,taskId){const tempDir=path.join(projectRoot,`${config.build.tempDirPrefix}-${taskId}`);if(fs.existsSync(tempDir)){fs.rmSync(tempDir,{recursive:true,force:true})}fs.mkdirSync(tempDir,{recursive:true});activeTempDirs.add(tempDir);return tempDir}/**
33
+ * 清理临时目录
34
+ */function cleanTempDir(tempDir){if(fs.existsSync(tempDir)){try{fs.rmSync(tempDir,{recursive:true,force:true});activeTempDirs.delete(tempDir);log(`已清理目录: ${path.basename(tempDir)}`,"debug")}catch(error){log(`清理临时目录失败: ${error.message}`,"warn")}}}/**
35
+ * 清理所有活动的临时目录
36
+ */function cleanAllTempDirs(){for(const tempDir of activeTempDirs){cleanTempDir(tempDir)}activeTempDirs.clear()}/**
37
+ * 递归复制目录
38
+ */function copyDirRecursive(src,dest){if(!fs.existsSync(dest)){fs.mkdirSync(dest,{recursive:true})}const entries=fs.readdirSync(src,{withFileTypes:true});for(const entry of entries){const srcPath=path.join(src,entry.name);const destPath=path.join(dest,entry.name);if(entry.isDirectory()){copyDirRecursive(srcPath,destPath)}else{fs.copyFileSync(srcPath,destPath)}}}/**
39
+ * 构建单个渠道(使用独立临时目录)
40
+ */async function buildChannel(config,themeName,channel,outputDir,projectRoot){const taskId=generateTaskId();const tempDir=createTempDir(config,projectRoot,taskId);log(`开始构建: 主题=${themeName}, 渠道=${channel}, 任务ID=${taskId.substr(0,8)}...`);try{// 生成文件名
41
+ const fileName=config.naming.generator?config.naming.generator(themeName,channel,getCurrentDate(),getCurrentMonth()):defaultFileNameGenerator(config,themeName,channel);log(`文件名: ${fileName}`,"debug");// 执行构建,指定独立的输出目录(使用绝对路径)
42
+ // 注意:
43
+ // 1. 命令行参数使用 --out-dir (连字符格式)
44
+ // 2. 通过环境变量 PLAYABLE_FILENAME 传递文件名,避免并发时修改 build.json 导致竞态条件
45
+ // 3. 使用 execAsync 实现真正的并发构建
46
+ // 4. 商店链接会自动从 builds.config.js 或 build.json 读取,无需通过命令行传递
47
+ await execAsync(`npm run build -- ${channel} --out-dir "${tempDir}"`,{cwd:projectRoot,env:{...process.env,PLAYABLE_BUILD_TASK_ID:taskId,PLAYABLE_FILENAME:fileName// 通过环境变量传递文件名
48
+ },// 不使用 stdio: 'inherit' 以支持并发,而是捕获输出
49
+ maxBuffer:10*1024*1024// 10MB buffer
50
+ });// 移动构建产物到目标目录
51
+ const targetDir=path.join(outputDir,themeName,channel);if(fs.existsSync(tempDir)){if(!fs.existsSync(targetDir)){fs.mkdirSync(targetDir,{recursive:true})}const files=fs.readdirSync(tempDir);files.forEach(file=>{const srcPath=path.join(tempDir,file);const destPath=path.join(targetDir,file);if(fs.statSync(srcPath).isDirectory()){copyDirRecursive(srcPath,destPath)}else{fs.copyFileSync(srcPath,destPath)}});log(`✓ 构建成功: ${themeName}/${channel}`,"success")}// 清理临时目录
52
+ if(config.build.cleanTemp){cleanTempDir(tempDir)}return{success:true,theme:themeName,channel}}catch(error){log(`构建失败: ${themeName}/${channel} - ${error.message}`,"error");// 清理临时目录
53
+ if(config.build.cleanTemp){cleanTempDir(tempDir)}if(!config.build.continueOnError){throw error}return{success:false,theme:themeName,channel,error:error.message}}}/**
54
+ * 并发构建多个渠道
55
+ */async function buildChannelsInParallel(config,themeName,channels,outputDir,projectRoot){const maxParallel=config.build.parallel||3;const results=[];// 分批并发构建
56
+ for(let i=0;i<channels.length;i+=maxParallel){const batch=channels.slice(i,i+maxParallel);const batchPromises=batch.map(channel=>buildChannel(config,themeName,channel,outputDir,projectRoot));const batchResults=await Promise.allSettled(batchPromises);batchResults.forEach((result,index)=>{if(result.status==="fulfilled"){results.push(result.value)}else{var _result$reason;results.push({success:false,theme:themeName,channel:batch[index],error:((_result$reason=result.reason)===null||_result$reason===void 0?void 0:_result$reason.message)||"\u672A\u77E5\u9519\u8BEF"})}})}return results}/**
57
+ * 解析命令行参数
58
+ */function parseArgs(){const args=process.argv.slice(2);const result={themes:null,channels:null,parallel:null};for(let i=0;i<args.length;i++){const currentArg=args[i];const nextArg=args[i+1];// 支持 -t 和 --themes 格式
59
+ if(currentArg==="-t"||currentArg==="--themes"){if(nextArg&&!nextArg.startsWith("-")){result.themes=nextArg.split(",").map(t=>t.trim()).filter(Boolean);i++}}// 支持 -c 和 --channels 格式
60
+ else if(currentArg==="-c"||currentArg==="--channels"){if(nextArg&&!nextArg.startsWith("-")){result.channels=nextArg.split(",").map(c=>c.trim()).filter(Boolean);i++}}// 支持 -p 和 --parallel 格式
61
+ else if(currentArg==="-p"||currentArg==="--parallel"){if(nextArg&&!nextArg.startsWith("-")){result.parallel=parseInt(nextArg,10);i++}}// 支持 --themes=value 格式(Windows PowerShell 兼容)
62
+ else if(currentArg.startsWith("--themes=")){result.themes=currentArg.substring(9).split(",").map(t=>t.trim()).filter(Boolean)}// 支持 --channels=value 格式(Windows PowerShell 兼容)
63
+ else if(currentArg.startsWith("--channels=")){result.channels=currentArg.substring(11).split(",").map(c=>c.trim()).filter(Boolean)}// 支持 --parallel=value 格式(Windows PowerShell 兼容)
64
+ else if(currentArg.startsWith("--parallel=")){result.parallel=parseInt(currentArg.substring(11),10)}// 兼容位置参数:第一个参数作为主题,第二个作为渠道
65
+ else if(!currentArg.startsWith("-")&&result.themes===null){result.themes=[currentArg]}else if(!currentArg.startsWith("-")&&result.themes!==null&&result.channels===null){result.channels=[currentArg]}}return result}// 远程配置仓库地址
66
+ const REMOTE_CONFIG_REPO="";/**
67
+ * 执行命令并返回结果
68
+ * @param {string} command - 要执行的命令
69
+ * @param {object} options - execSync 选项
70
+ * @returns {string|null} 命令输出,失败返回 null
71
+ */function execCommand(command,options={}){const{execSync}=require("child_process");try{return execSync(command,{encoding:"utf-8",stdio:["pipe","pipe","pipe"],...options}).trim()}catch(error){return null}}/**
72
+ * 从远程仓库获取配置文件内容
73
+ * 使用 git clone 方式获取,读取后删除临时目录
74
+ *
75
+ * @param {string} appName - 应用名称,对应远程仓库中的文件夹名
76
+ * @returns {Promise<object|null>} 远程配置对象,失败返回 null
77
+ */async function fetchRemoteConfig(appName){if(!appName||!REMOTE_CONFIG_REPO)return null;const os=require("os");// 使用系统临时目录
78
+ const tempDir=path.join(os.tmpdir(),`playable-config-${Date.now()}`);log(`尝试从远程仓库拉取配置: ${appName}`,"info");log(`仓库地址: ${REMOTE_CONFIG_REPO}`,"debug");try{// 创建临时目录
79
+ fs.mkdirSync(tempDir,{recursive:true});// 克隆远程仓库到临时目录
80
+ log("\u514B\u9686\u8FDC\u7A0B\u914D\u7F6E\u4ED3\u5E93...","info");const cloneResult=execCommand(`git clone --depth 1 --quiet "${REMOTE_CONFIG_REPO}" "${tempDir}"`,{timeout:30000});if(cloneResult===null){log("git clone \u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u548C\u4ED3\u5E93\u6743\u9650","error");return null}log("\u2713 \u8FDC\u7A0B\u914D\u7F6E\u4ED3\u5E93\u514B\u9686\u6210\u529F","success");// 检查 appName 对应的文件夹是否存在
81
+ const appConfigDir=path.join(tempDir,appName);if(!fs.existsSync(appConfigDir)){log(`远程仓库中未找到 "${appName}" 文件夹`,"warn");// 列出可用的配置文件夹供参考
82
+ const availableDirs=fs.readdirSync(tempDir).filter(name=>{const itemPath=path.join(tempDir,name);return fs.statSync(itemPath).isDirectory()&&!name.startsWith(".")});if(availableDirs.length>0){log(`可用的 appName: ${availableDirs.join(", ")}`,"info")}return null}// 检查配置文件是否存在
83
+ const configFilePath=path.join(appConfigDir,"build.config.js");if(!fs.existsSync(configFilePath)){log(`"${appName}" 文件夹中未找到 build.config.js`,"warn");return null}// 读取配置文件内容并解析
84
+ // 使用 vm 模块执行,避免 require 缓存问题
85
+ const vm=require("vm");const configContent=fs.readFileSync(configFilePath,"utf-8");const sandbox={module:{exports:{}},exports:{},require:require};sandbox.exports=sandbox.module.exports;try{vm.runInNewContext(configContent,sandbox);log(`✓ 成功加载远程配置: ${appName}`,"success");return sandbox.module.exports}catch(parseError){log(`解析远程配置文件失败: ${parseError.message}`,"error");return null}}catch(error){log(`获取远程配置失败: ${error.message}`,"warn");return null}finally{// 无论成功与否,都删除临时目录
86
+ if(fs.existsSync(tempDir)){try{fs.rmSync(tempDir,{recursive:true,force:true});log("\u5DF2\u6E05\u7406\u4E34\u65F6\u76EE\u5F55","debug")}catch(cleanupError){log(`清理临时目录失败: ${cleanupError.message}`,"debug")}}}}/**
87
+ * 加载配置文件
88
+ *
89
+ * 优先级: 本地 builds.config.js > 远程配置 > build.json > 默认值
90
+ *
91
+ * 配置合并逻辑:
92
+ * 1. 首先读取本地 builds.config.js
93
+ * 2. 如果配置了 appName,从远程仓库拉取对应配置并合并(本地优先)
94
+ * 3. 读取 build.json 作为补充(向后兼容)
95
+ *
96
+ * 兼容性说明:
97
+ * - builds.config.js: 新的推荐配置文件,支持所有配置项
98
+ * - build.json: 旧的配置文件,向后兼容,仅读取 app, name, google_play_url, app_store_url
99
+ *
100
+ * 字段映射:
101
+ * - build.json.app + build.json.name → title (如果 builds.config.js 未指定)
102
+ * - build.json.google_play_url → googlePlayUrl
103
+ * - build.json.app_store_url → appStoreUrl
104
+ *
105
+ * 忽略字段:
106
+ * - filename: 由批量构建自动生成
107
+ * - version: 不在批量构建中使用
108
+ */async function loadConfig(projectRoot){var _localConfig;const buildsConfigPath=path.join(projectRoot,"builds.config.js");const buildJsonPath=path.join(projectRoot,"build.json");let config={...DEFAULT_CONFIG};let localConfig=null;// 1. 先尝试加载本地 builds.config.js
109
+ if(fs.existsSync(buildsConfigPath)){log("\u52A0\u8F7D\u914D\u7F6E\u6587\u4EF6: builds.config.js","debug");// 清除 require 缓存,确保读取最新配置
110
+ delete require.cache[buildsConfigPath];localConfig=require(buildsConfigPath)}else{log("\u672A\u627E\u5230 builds.config.js\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u914D\u7F6E","debug")}// 2. 如果配置了 appName,从远程仓库拉取配置并合并
111
+ const appName=(_localConfig=localConfig)===null||_localConfig===void 0?void 0:_localConfig.appName;if(appName){const remoteConfig=await fetchRemoteConfig(appName);if(remoteConfig){// 远程配置先合并到默认配置
112
+ config=mergeConfig(config,remoteConfig);log(`已合并远程配置: ${appName}`,"info")}}// 3. 本地配置具有最高优先级,覆盖远程配置
113
+ if(localConfig){config=mergeConfig(config,localConfig)}// 5. 尝试加载 build.json 作为补充(向后兼容)
114
+ if(fs.existsSync(buildJsonPath)){try{const buildJson=JSON.parse(fs.readFileSync(buildJsonPath,"utf8"));log("\u68C0\u6D4B\u5230 build.json\uFF0C\u8BFB\u53D6\u517C\u5BB9\u914D\u7F6E","debug");// 合并 app + name 为 title(仅当 builds.config.js 未指定时)
115
+ if(!config.title||config.title===DEFAULT_CONFIG.title){if(buildJson.app&&buildJson.name){config.title=`${buildJson.app} ${buildJson.name}`;log(` ├─ title: "${config.title}" (来自 build.json)`,"debug")}else if(buildJson.app){config.title=buildJson.app;log(` ├─ title: "${config.title}" (来自 build.json.app)`,"debug")}else if(buildJson.name){config.title=buildJson.name;log(` ├─ title: "${config.title}" (来自 build.json.name)`,"debug")}}// 读取商店链接(如果 builds.config.js 未指定)
116
+ if(buildJson.google_play_url&&!config.googlePlayUrl){config.googlePlayUrl=buildJson.google_play_url;log(` ├─ googlePlayUrl: "${config.googlePlayUrl}" (来自 build.json)`,"debug")}if(buildJson.app_store_url&&!config.appStoreUrl){config.appStoreUrl=buildJson.app_store_url;log(` └─ appStoreUrl: "${config.appStoreUrl}" (来自 build.json)`,"debug")}// 忽略的字段
117
+ if(buildJson.filename){log(" \u26A0\uFE0F build.json.filename \u5C06\u88AB\u5FFD\u7565\uFF08\u7531\u6279\u91CF\u6784\u5EFA\u81EA\u52A8\u751F\u6210\uFF09","debug")}if(buildJson.version){log(" \u26A0\uFE0F build.json.version \u5C06\u88AB\u5FFD\u7565\uFF08\u4E0D\u5728\u6279\u91CF\u6784\u5EFA\u4E2D\u4F7F\u7528\uFF09","debug")}}catch(error){log(`⚠️ 解析 build.json 失败: ${error.message}`,"warn")}}return config}/**
118
+ * 深度合并配置
119
+ */function mergeConfig(defaultConfig,userConfig){const result={...defaultConfig};for(const key in userConfig){if(typeof userConfig[key]==="object"&&!Array.isArray(userConfig[key])){result[key]={...defaultConfig[key],...userConfig[key]}}else{result[key]=userConfig[key]}}return result}/**
120
+ * 打印配置摘要
121
+ */function printSummary(config,themes,channels){var _os$cpus$;const totalBuilds=themes.length*channels.length;const cpuCount=os.cpus().length;const cpuModel=((_os$cpus$=os.cpus()[0])===null||_os$cpus$===void 0?void 0:_os$cpus$.model)||"Unknown";log("========================================");log("\u5F00\u59CB\u6279\u91CF\u6784\u5EFA\u4EFB\u52A1");log(`项目: ${config.title}`);log(`主题数: ${themes.length}`);log(`渠道数: ${channels.length}`);log(`总构建数: ${totalBuilds}`);log(`CPU: ${cpuCount} 核心 (${cpuModel.substring(0,40)}${cpuModel.length>40?"...":""})`);log(`并发数: ${config.build.parallel} (自动优化)`);log("========================================\n");if(themes.length<=10){log(`主题列表: ${themes.join(", ")}`,"debug")}log(`渠道列表: ${channels.join(", ")}`,"debug");log("")}// ==================== 主函数 ====================
122
+ async function buildBatch(){const projectRoot=process.cwd();const startTime=Date.now();try{// 加载配置(包含远程配置合并)
123
+ const config=await loadConfig(projectRoot);// 解析命令行参数
124
+ const args=parseArgs();// 应用命令行参数
125
+ if(args.parallel!==null){config.build.parallel=args.parallel}// 确定主题列表
126
+ let themes;if(!config.themes.enabled){themes=[config.title.replace(/\s+/g,"-")]}else if(args.themes){themes=args.themes;log(`使用命令行指定的主题: ${themes.join(", ")}`,"info")}else{themes=getAllThemes(config,projectRoot);if(themes.length===0){throw new Error("\u672A\u627E\u5230\u4EFB\u4F55\u4E3B\u9898")}}// 确定渠道列表
127
+ let channels=args.channels||config.channels;// 检查是否需要生成预览链接
128
+ const needPreview=config.cosConfig&&config.cosConfig.enabled!==false&&config.cosConfig.SecretId&&config.cosConfig.SecretKey&&config.cosConfig.Bucket&&config.cosConfig.Region&&config.cosConfig.CdnDomain;// 如果配置了 COS,添加 preview 渠道
129
+ if(needPreview){if(!channels.includes("preview")){channels=[...channels,"preview"];log("\u68C0\u6D4B\u5230 COS \u914D\u7F6E\uFF0C\u5DF2\u6DFB\u52A0 preview \u6E20\u9053","info")}}// 打印摘要
130
+ printSummary(config,themes,channels);// 备份入口文件(如果启用主题切换)
131
+ if(config.themes.enabled){backupEntryFile(config,projectRoot)}// 批量构建
132
+ const outputDir=path.join(projectRoot,config.build.outputDir);const allResults=[];const previewLinks=[];// 存储预览链接
133
+ // 批量构建前,清理一下目录
134
+ cleanTempDir(outputDir);for(const theme of themes){// 切换主题
135
+ if(config.themes.enabled){switchTheme(config,theme,projectRoot)}// 并发构建该主题的所有渠道
136
+ const themeResults=await buildChannelsInParallel(config,theme,channels,outputDir,projectRoot);allResults.push(...themeResults);// 如果需要预览链接,上传 preview 渠道产物到 COS
137
+ if(needPreview){const previewResult=themeResults.find(r=>r.channel==="preview"&&r.success);if(previewResult){try{const previewDir=path.join(outputDir,theme,"preview");// 去掉 theme 这一级目录,直接使用 PathPrefix
138
+ const cosPrefix=config.cosConfig.PathPrefix||"";log(`开始上传 ${theme} 预览文件到 COS...`,"info");const uploadResults=await uploadDirectoryToCOS(config.cosConfig,previewDir,cosPrefix);// 保存预览链接
139
+ uploadResults.forEach(result=>{previewLinks.push({theme,file:result.file,url:result.url})});log(`✓ ${theme} 预览文件上传成功`,"success")}catch(error){log(`上传 ${theme} 预览文件失败: ${error.message}`,"error")}}}// 打印进度
140
+ const completedBuilds=allResults.length;const totalBuilds=themes.length*channels.length;const progress=Math.round(completedBuilds/totalBuilds*100);log(`进度: ${completedBuilds}/${totalBuilds} (${progress}%)`,"success")}// 恢复入口文件
141
+ if(config.themes.enabled){restoreEntryFile()}// 确保清理所有临时目录
142
+ if(config.build.cleanTemp){cleanAllTempDirs()}// 统计结果
143
+ const successBuilds=allResults.filter(r=>r.success).length;const failedBuilds=allResults.filter(r=>!r.success);// 打印结果
144
+ const duration=((Date.now()-startTime)/1000/60).toFixed(2);log("\n========================================");log("\uD83C\uDF89 \u6240\u6709\u6784\u5EFA\u4EFB\u52A1\u5B8C\u6210\uFF01","success");log(`✓ 成功构建: ${successBuilds}/${allResults.length}`);if(failedBuilds.length>0){log(`✗ 失败构建: ${failedBuilds.length}`,"warn");failedBuilds.forEach(({theme,channel,error})=>{log(` - ${theme}/${channel}: ${error}`,"error")})}log(`⏱️ 耗时: ${duration} 分钟`);log(`📁 输出目录: ${outputDir}`);// 打印预览链接
145
+ if(previewLinks.length>0){log("\n========================================");log("\uD83D\uDD17 \u9884\u89C8\u94FE\u63A5","info");log("========================================");// 按主题分组
146
+ const linksByTheme={};previewLinks.forEach(link=>{if(!linksByTheme[link.theme]){linksByTheme[link.theme]=[]}linksByTheme[link.theme].push(link)});// 打印链接
147
+ Object.keys(linksByTheme).sort().forEach(theme=>{log(`\n📦 ${theme}:`,"info");linksByTheme[theme].forEach(link=>{console.log(`${link.url}`)})});log("\n========================================")}log("========================================")}catch(error){log(`严重错误: ${error.message}`,"error");console.error(error);restoreEntryFile();cleanAllTempDirs();process.exit(1)}}// ==================== 信号处理 ====================
148
+ function cleanup(){if(isExiting)return;isExiting=true;log("\n\u6536\u5230\u4E2D\u65AD\u4FE1\u53F7\uFF0C\u6B63\u5728\u6E05\u7406...","warn");restoreEntryFile();cleanAllTempDirs();process.exit(1)}process.on("SIGINT",cleanup);process.on("SIGTERM",cleanup);// ==================== 导出 ====================
149
+ module.exports={buildBatch};
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ "use strict";/**
3
+ * COS 上传模块
4
+ *
5
+ * 功能:上传文件到腾讯云 COS,并刷新 CDN 缓存
6
+ */const COS=require("cos-nodejs-sdk-v5");const fs=require("fs");const path=require("path");/**
7
+ * 创建 COS 客户端
8
+ */function createCOSClient(cosConfig){if(!cosConfig||!cosConfig.SecretId||!cosConfig.SecretKey){throw new Error("COS \u914D\u7F6E\u7F3A\u5931\uFF1A\u9700\u8981 SecretId \u548C SecretKey")}return new COS({SecretId:cosConfig.SecretId,SecretKey:cosConfig.SecretKey})}/**
9
+ * 刷新 CDN 缓存
10
+ *
11
+ * @param {Object} cosConfig - COS 配置
12
+ * @param {Array<string>} urls - 需要刷新的 URL 列表
13
+ * @returns {Promise<void>}
14
+ */async function purgeCDNCache(cosConfig,urls){if(!urls||urls.length===0){return}// 如果没有配置 CDN 刷新,跳过
15
+ if(!cosConfig.CdnDomain){console.log("\u26A0\uFE0F \u672A\u914D\u7F6E CdnDomain\uFF0C\u8DF3\u8FC7 CDN \u5237\u65B0");return}try{// 动态加载 CDN SDK
16
+ const tencentcloud=require("tencentcloud-sdk-nodejs-cdn");const CdnClient=tencentcloud.cdn.v20180606.Client;const clientConfig={credential:{secretId:cosConfig.SecretId,secretKey:cosConfig.SecretKey},region:"",// CDN 是全球服务,不需要指定 region
17
+ profile:{httpProfile:{endpoint:"cdn.tencentcloudapi.com"}}};const client=new CdnClient(clientConfig);// CDN 刷新区域(从配置中读取,默认为 overseas)
18
+ // mainland: 中国境内
19
+ // overseas: 中国境外
20
+ // global: 全球
21
+ const area=cosConfig.CdnArea||"overseas";// 每次最多刷新 1000 个 URL(腾讯云限制)
22
+ const batchSize=1000;for(let i=0;i<urls.length;i+=batchSize){const batch=urls.slice(i,i+batchSize);const params={Urls:batch,Area:area};await client.PurgeUrlsCache(params);console.log(`✓ 已刷新 ${batch.length} 个 URL 的 CDN 缓存 (区域: ${area})`)}}catch(error){// 如果没有安装 CDN SDK,给出提示
23
+ if(error.code==="MODULE_NOT_FOUND"){console.warn("\u26A0\uFE0F \u672A\u5B89\u88C5 tencentcloud-sdk-nodejs-cdn\uFF0C\u8DF3\u8FC7 CDN \u5237\u65B0");console.warn(" \u5B89\u88C5\u547D\u4EE4: npm install tencentcloud-sdk-nodejs-cdn --save-dev")}else{console.error(`CDN 刷新失败: ${error.message}`);// 不抛出错误,避免影响上传流程
24
+ }}}/**
25
+ * 上传单个文件到 COS
26
+ *
27
+ * @param {Object} cosConfig - COS 配置
28
+ * @param {string} filePath - 本地文件路径
29
+ * @param {string} cosKey - COS 对象键(远程路径)
30
+ * @returns {Promise<string>} - 上传成功返回访问链接
31
+ */async function uploadFileToCOS(cosConfig,filePath,cosKey){if(!fs.existsSync(filePath)){throw new Error(`文件不存在: ${filePath}`)}const cos=createCOSClient(cosConfig);return new Promise((resolve,reject)=>{cos.putObject({Bucket:cosConfig.Bucket,Region:cosConfig.Region,Key:cosKey,Body:fs.createReadStream(filePath)},(err,data)=>{if(err){reject(new Error(`上传失败: ${err.message}`))}else{// 生成访问链接
32
+ const cdnDomain=cosConfig.CdnDomain;// 对路径的每个部分分别进行 URL 编码
33
+ const pathParts=cosKey.split("/");const encodedPath=pathParts.map(part=>encodeURIComponent(part)).join("/");const url=`https://${cdnDomain}/${encodedPath}`;resolve(url)}})})}/**
34
+ * 上传目录下的所有文件到 COS
35
+ *
36
+ * @param {Object} cosConfig - COS 配置
37
+ * @param {string} localDir - 本地目录路径
38
+ * @param {string} cosPrefix - COS 路径前缀
39
+ * @returns {Promise<Array>} - 上传成功返回文件列表
40
+ */async function uploadDirectoryToCOS(cosConfig,localDir,cosPrefix){if(!fs.existsSync(localDir)){throw new Error(`目录不存在: ${localDir}`)}const files=fs.readdirSync(localDir);const results=[];const uploadedUrls=[];for(const file of files){const filePath=path.join(localDir,file);const stat=fs.statSync(filePath);if(stat.isFile()){// 去掉文件名中的 -preview 或 _preview 后缀
41
+ let cleanFileName=file;cleanFileName=cleanFileName.replace(/-preview\./i,".");cleanFileName=cleanFileName.replace(/_preview\./i,".");const cosKey=cosPrefix?`${cosPrefix}/${cleanFileName}`:cleanFileName;const url=await uploadFileToCOS(cosConfig,filePath,cosKey);results.push({file:cleanFileName,url});uploadedUrls.push(url)}}// 上传完成后刷新 CDN 缓存
42
+ if(uploadedUrls.length>0){await purgeCDNCache(cosConfig,uploadedUrls)}return results}module.exports={createCOSClient,uploadFileToCOS,uploadDirectoryToCOS,purgeCDNCache};
@@ -0,0 +1,167 @@
1
+ import { Configuration as WebpackConfig } from 'webpack';
2
+ import { WebpackPluginInstance } from 'webpack';
3
+
4
+ /** Supported file extensions for webpack resolution */
5
+ export declare const allowedExtensions: string[];
6
+
7
+ /** Base webpack configuration used by both development and build configs */
8
+ export declare const webpackCommonConfig: WebpackConfig;
9
+
10
+ /** Options for CLI arguments */
11
+ export interface CLIOptionConfig {
12
+ name: string;
13
+ alias?: string;
14
+ hasValue?: boolean;
15
+ defaultValue?: any;
16
+ description: string;
17
+ parser?: (value: string) => any;
18
+ }
19
+
20
+ /** Parse command line arguments based on configuration */
21
+ export declare function parseArgvOptions(possibleOptions: CLIOptionConfig[]): Record<string, any>;
22
+
23
+ /** Deep merge configuration options */
24
+ export declare function mergeOptions<T>(target: T, source?: Partial<T>): T;
25
+
26
+ /** Configuration for webpack defines */
27
+ export interface DefinesOptions {
28
+ /** Development mode flag for conditional code */
29
+ __DEV__: string;
30
+ /** Google Play Store URL constant */
31
+ GOOGLE_PLAY_URL: string;
32
+ /** App Store URL constant */
33
+ APP_STORE_URL: string;
34
+ /** Current advertising network identifier */
35
+ AD_NETWORK: string;
36
+ /** Current advertising protocol */
37
+ AD_PROTOCOL: string;
38
+ /** Application name constant */
39
+ APP: string;
40
+ /** Build name constant */
41
+ NAME: string;
42
+ /** Build version constant */
43
+ VERSION: string;
44
+ /** Unique build identifier generated from timestamp */
45
+ BUILD_HASH: string;
46
+ /** Current language code */
47
+ LANGUAGE: string;
48
+ /** Current orientation identifier */
49
+ ORIENTATION: 'both' | 'portrait' | 'landscape' | 'square';
50
+ }
51
+
52
+ /** CLI configuration options */
53
+ export interface CLIOptions {
54
+ /** Output directory for build files (default: 'dist') */
55
+ outDir: string;
56
+ /** Path to build.json configuration file (default: 'build.json') */
57
+ buildConfig: string;
58
+ /** Path to tsconfig.json configuration file (default: 'tsconfig.json') */
59
+ tsConfig: string;
60
+ /** Path to jsconfig.json configuration file (default: 'jsconfig.json') */
61
+ jsConfig: string;
62
+ /** Development server port number (default: 3000) */
63
+ port: number;
64
+ /** Whether to open browser automatically (default: false) */
65
+ open: boolean;
66
+ /** Ad protocol to use (default: 'none') */
67
+ protocol: 'none' | 'mraid' | 'dapi';
68
+ /** Ad network identifier (default: 'preview') */
69
+ network:
70
+ | 'preview'
71
+ | 'applovin'
72
+ | 'unity'
73
+ | 'google'
74
+ | 'ironsource'
75
+ | 'facebook'
76
+ | 'moloco'
77
+ | 'mintegral'
78
+ | 'vungle'
79
+ | 'adcolony'
80
+ | 'tapjoy'
81
+ | 'snapchat'
82
+ | 'tiktok'
83
+ | 'appreciate'
84
+ | 'chartboost'
85
+ | 'pangle'
86
+ | 'mytarget'
87
+ | 'liftoff'
88
+ | 'smadex'
89
+ | 'adikteev'
90
+ | 'bigabid'
91
+ | 'inmobi';
92
+ /** Development mode flag */
93
+ dev: boolean;
94
+ /** Skip recommended meta tags injection */
95
+ skipRecommendedMeta?: boolean;
96
+ /** URL of debugger script to inject */
97
+ debugger?: string;
98
+ /** Code obfuscation level: 1=terser only, 2=obfuscator basic, 3=obfuscator+dead code, 4=maximum (default: 2) */
99
+ obfuscateLevel: 1 | 2 | 3 | 4;
100
+ /** Template for output filename using pattern {app}_{name}_{version}_{date}_{language}_{network} */
101
+ filename: string;
102
+ /** Application name used in build filename and APP define */
103
+ app: string;
104
+ /** Concept name used in build filename and NAME define */
105
+ name: string;
106
+ /** Version name used in build filename and VERSION define */
107
+ version: string;
108
+ /** Language code for localization */
109
+ language: 'auto' | 'en' | 'es' | 'zh' | 'hi' | 'ar' | 'fr' | 'de' | 'ja' | 'pt';
110
+ /** Orientation identifier */
111
+ orientation: 'both' | 'portrait' | 'landscape' | 'square';
112
+ /** Google Play Store URL for the app */
113
+ googlePlayUrl: string;
114
+ /** App Store URL for the app */
115
+ appStoreUrl: string;
116
+ /** Webpack define plugin configuration */
117
+ defines: Record<string, string>;
118
+ }
119
+
120
+ /** Global options object */
121
+ export declare const options: CLIOptions;
122
+
123
+ /** Create webpack config for development */
124
+ export declare function makeWebpackDevConfig(
125
+ customOptions?: Partial<CLIOptions>,
126
+ customDefines?: Record<string, any>,
127
+ webpackCustomConfig?: Partial<WebpackConfig>
128
+ ): WebpackConfig;
129
+
130
+ /** Create webpack config for production build */
131
+ export declare function makeWebpackBuildConfig(
132
+ customOptions?: Partial<CLIOptions>,
133
+ customDefines?: Record<string, any>,
134
+ webpackCustomConfig?: Partial<WebpackConfig>
135
+ ): WebpackConfig;
136
+
137
+ /** Start webpack development server */
138
+ export declare function runDev(
139
+ webpackConfig?: WebpackConfig,
140
+ customOptions?: Partial<CLIOptions>,
141
+ customDefines?: Record<string, any>,
142
+ webpackCustomConfig?: Partial<WebpackConfig>
143
+ ): void;
144
+
145
+ /** Run webpack production build */
146
+ export declare function runBuild(
147
+ webpackConfig?: WebpackConfig,
148
+ customOptions?: Partial<CLIOptions>,
149
+ customDefines?: Record<string, any>,
150
+ webpackCustomConfig?: Partial<WebpackConfig>
151
+ ): void;
152
+
153
+ /** Plugin for injecting DAPI script */
154
+ export declare class DAPIInjectorPlugin implements WebpackPluginInstance {
155
+ apply(compiler: any): void;
156
+ }
157
+
158
+ /** Plugin for injecting debugger script */
159
+ export declare class DebuggerInjectionPlugin implements WebpackPluginInstance {
160
+ constructor(debuggerSrc: string);
161
+ apply(compiler: any): void;
162
+ }
163
+
164
+ /** Plugin for injecting Google's ExitAPI and meta tags */
165
+ export declare class ExitAPIInjectorPlugin implements WebpackPluginInstance {
166
+ apply(compiler: any): void;
167
+ }
package/core/index.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";const{allowedExtensions,webpackCommonConfig}=require("./webpack.common.js");const{parseArgvOptions}=require("./utils/parseArgvOptions.js");const{mergeOptions}=require("./utils/mergeOptions.js");const{options}=require("./options.js");const{DAPIInjectorPlugin}=require("./plugins/DAPIInjectorPlugin.js");const{DebuggerInjectionPlugin}=require("./plugins/DebuggerInjectionPlugin.js");const{ExitAPIInjectorPlugin}=require("./plugins/ExitAPIInjectorPlugin.js");const{runBuild,makeWebpackBuildConfig}=require("./webpack.build.js");const{runDev,makeWebpackDevConfig}=require("./webpack.dev.js");exports.mergeOptions=mergeOptions;exports.parseArgvOptions=parseArgvOptions;exports.options=options;exports.allowedExtensions=allowedExtensions;exports.webpackCommonConfig=webpackCommonConfig;exports.makeWebpackDevConfig=makeWebpackDevConfig;exports.makeWebpackBuildConfig=makeWebpackBuildConfig;exports.runDev=runDev;exports.runBuild=runBuild;exports.DAPIInjectorPlugin=DAPIInjectorPlugin;exports.ExitAPIInjectorPlugin=ExitAPIInjectorPlugin;exports.DebuggerInjectionPlugin=DebuggerInjectionPlugin;
@@ -0,0 +1,45 @@
1
+ "use strict";const path=require("path");const loaderUtils=require("loader-utils");const fs=require("fs");const isObject=value=>typeof value==="object"&&value!==null;const isDataURI=value=>/^data:/i.test(value);/**
2
+ * A utility function that allows mapping the value for a given key in an object
3
+ * [in place](https://en.wikipedia.org/wiki/In-place_algorithm).
4
+ *
5
+ * @remarks
6
+ * This function recursively iterates through every key in the object.
7
+ * Once a key strictly equal to the given key is found, the callback function
8
+ * is invoked, and its return value (or resolved value) is then set as the new value
9
+ * for that entry.
10
+ *
11
+ * @param {unknown} obj - The object to mutate.
12
+ * @param {string} key - The key to match against.
13
+ * @param {(value: unknown) => Promise<any>} callback
14
+ * The callback that either returns the mapped value, or returns a Promise
15
+ * that resolves to the new value.
16
+ */async function mapDeep(obj,key,callback){if(Array.isArray(obj)){for(const item of obj){await mapDeep(item,key,callback)}}else if(isObject(obj)){for(const[k,v]of Object.entries(obj)){if(k===key){obj[k]=await callback(v)}else{await mapDeep(v,key,callback)}}}}/**
17
+ * @typedef {import(".").GLTFLoaderDefinition} GLTFLoader
18
+ * @this {ThisParameterType<GLTFLoader>}
19
+ * @type {GLTFLoader}
20
+ */async function gltfLoader(content){// Destructure loader options and set default values:
21
+ const{// inline = true,
22
+ // useRelativePaths = true,
23
+ // uriResolver = (module) => String(module.default ?? module),
24
+ // fileName = '[name].[hash:8].[ext]',
25
+ // filePath = '/static/media',
26
+ // publicPath = this._compilation?.outputOptions.publicPath ?? '/',
27
+ context=this.context}=this.getOptions();// Parse the glTF data:
28
+ // console.log(content);
29
+ const data=JSON.parse(content);// console.log(data);
30
+ // Iterate over the object and map any URIs:
31
+ await mapDeep(data,"uri",async uri=>{// Resolve early if the URI cannot be imported as a module:
32
+ if(!loaderUtils.isUrlRequest(uri))return uri;// If the URI is a data URI, print a warning and resolve with the original value:
33
+ if(isDataURI(uri)){return uri}const requestPath=path.join(context,uri);// console.log(requestPath);
34
+ const binContent=fs.readFileSync(requestPath,{encoding:"base64"});return`data:application/octet-stream;base64,${binContent}`});// Stringify the updated data:
35
+ const updatedContent=JSON.stringify(data);// Interpolate any file name tokens:
36
+ // const interpolatedName = loaderUtils.interpolateName(this, fileName, { content: updatedContent });
37
+ // Join the file path and interpolated name:
38
+ // const interpolatedPath = path.join(filePath, interpolatedName);
39
+ // Emit the file:
40
+ // this.emitFile(interpolatedPath, updatedContent, null);
41
+ // Join all paths together:
42
+ // const fullPath = path.join(publicPath, interpolatedPath);
43
+ // Lastly, resolve with either the JSON data or the full output path:
44
+ // return `export default ${inline ? updatedContent : `"${fullPath}"`}`;
45
+ return`export default ${updatedContent}`}module.exports=gltfLoader;
@@ -0,0 +1,25 @@
1
+ "use strict";const{parseArgvOptions,allowedAdProtocols,allowedAdNetworks,allowedOrientations,allowedLanguages}=require("./utils/parseArgvOptions");const path=require("path");const fs=require("fs");const{name,version}=require("../package.json");const possibleOptions=[{name:"out-dir",alias:"outDir",defaultValue:"dist",hasValue:true,description:"Output directory for build files"},{name:"build-config",alias:"buildConfig",defaultValue:"build.json",hasValue:true,description:"Path to build.json configuration file"},{name:"ts-config",alias:"tsConfig",defaultValue:"tsconfig.json",hasValue:true,description:"For TypeScript projects, path to tsconfig.json file"},{name:"js-config",alias:"jsConfig",defaultValue:"jsconfig.json",hasValue:true,description:"For JavaScript projects, path to jsconfig.json file"},{name:"port",hasValue:true,defaultValue:3000,description:"Development server port number",parser:function(rawValue){const value=+rawValue;if(isNaN(value))throw new Error("--port should be a number");return value}},{name:"open",defaultValue:false,hasValue:false,description:"Open browser automatically when server starts"},{name:"protocol",hasValue:true,defaultValue:"none",description:"Ad protocol to use (none, mraid, or dapi)",parser:function(rawValue){if(!allowedAdProtocols.includes(rawValue)){throw new Error(`--protocol should have one of the value: ${allowedAdProtocols.join(", ")}`)}return rawValue}},{name:"network",defaultValue:"preview",hasValue:true,description:"Target Ad network",parser:function(rawValue){if(!allowedAdNetworks.includes(rawValue)){throw new Error(`--network should have one of the value: ${allowedAdNetworks.join(", ")}`)}return rawValue}},{name:"zip",defaultValue:false,hasValue:false,description:"Should the build be zipped? (only for some ad networks)"},{name:"dev",hasValue:true,description:"Enable development mode (true/false)",parser:function(rawValue){if(!(rawValue==="true"||rawValue==="false"))throw new Error("--dev should have either true or false value");return rawValue==="true"}},{name:"filename",defaultValue:"{app}_{name}_{version}_{date}_{language}_{network}",hasValue:true,description:"Specifies the build filename template"},{name:"app",defaultValue:"AppName",hasValue:true,description:"Specifies the application name used in build filename and APP define"},{name:"name",defaultValue:"ConceptName",hasValue:true,description:"Specifies the concept name used in build filename and NAME define"},{name:"version",defaultValue:"v1",hasValue:true,description:"Specifies the version name used in build filename and VERSION define"},{name:"language",defaultValue:"en",hasValue:true,description:"Specifies the language of the build used in LANGUAGE define",parser:function(rawValue){if(!allowedLanguages.includes(rawValue)){throw new Error(`--platform should have one of the value: ${allowedLanguages.join(", ")}`)}return rawValue}},{name:"orientation",defaultValue:"both",hasValue:true,description:"Specifies the ad orientation used in ORIENTATION define and some network specific configurations",parser:function(rawValue){if(!allowedOrientations.includes(rawValue)){throw new Error(`--orientation should have one of the value: ${allowedOrientations.join(", ")}`)}return rawValue}},{name:"google-play-url",alias:"googlePlayUrl",defaultValue:"https://play.google.com/store/games",hasValue:true,description:"Google Play Store URL for the app"},{name:"app-store-url",alias:"appStoreUrl",defaultValue:"https://www.apple.com/app-store/",hasValue:true,description:"App Store URL for the app"},{name:"skip-recommended-meta",alias:"skipRecommendedMeta",hasValue:false,description:"Don't inject recommended for playable ads META tags"},{name:"debugger",hasValue:true,description:"URL of debugger script to inject into code"},{name:"obfuscate-level",alias:"obfuscateLevel",defaultValue:4,hasValue:true,description:"Code obfuscation level: 1=terser only, 2=obfuscator basic(default), 3=obfuscator+dead code, 4=maximum obfuscation",parser:function(rawValue){const value=+rawValue;if(isNaN(value)||value<1||value>4){throw new Error("--obfuscate-level should be a number between 1 and 4")}return value}},{name:"fflate-compression",alias:"fflateCompression",defaultValue:false,hasValue:true,description:"Enable fflate compression for HTML output (level 9, production only)",parser:function(rawValue){// 支持 true/false 字符串和布尔值
2
+ if(rawValue==="true"||rawValue===true||rawValue==="1")return true;if(rawValue==="false"||rawValue===false||rawValue==="0")return false;return Boolean(rawValue)}}];/** @type {import('./index').CLIOptions} */const options=parseArgvOptions(possibleOptions);options.defines={};options.compilation={};options.adNetworkNames={};// ==================== 配置文件加载 ====================
3
+ //
4
+ // 优先级(从低到高):
5
+ // 1. builds.config.js (新配置文件,优先读取)
6
+ // 2. build.json (旧配置文件,作为覆盖/补充)
7
+ // 3. 命令行参数 (最高优先级)
8
+ //
9
+ // 字段映射:
10
+ // - builds.config.js: googlePlayUrl, appStoreUrl
11
+ // - build.json: google_play_url → googlePlayUrl, app_store_url → appStoreUrl
12
+ // 1️⃣ 先尝试读取 builds.config.js
13
+ const buildsConfigPath=path.resolve("builds.config.js");if(fs.existsSync(buildsConfigPath)){try{// 清除 require 缓存,确保读取最新配置
14
+ delete require.cache[buildsConfigPath];const buildsConfig=require(buildsConfigPath);// 读取商店链接配置
15
+ if(buildsConfig.googlePlayUrl){options.googlePlayUrl=buildsConfig.googlePlayUrl}if(buildsConfig.appStoreUrl){options.appStoreUrl=buildsConfig.appStoreUrl}// 读取其他特殊配置
16
+ if(buildsConfig.defines)Object.assign(options.defines,buildsConfig.defines);if(buildsConfig.compilation)Object.assign(options.compilation,buildsConfig.compilation);if(buildsConfig.adNetworkNames)Object.assign(options.adNetworkNames,buildsConfig.adNetworkNames);// 读取所有通用配置字段(如果在 builds.config.js 中定义了)
17
+ for(const key in buildsConfig){// 跳过已处理的特殊字段和批量构建专用字段
18
+ if(["googlePlayUrl","appStoreUrl","defines","compilation","adNetworkNames","title","appName","projectCode","channels","themes","naming","build","hooks","cosConfig"].includes(key)){continue}// 查找对应的配置项定义
19
+ const possibleOption=possibleOptions.find(e=>e.alias===key||e.name===key);if(possibleOption&&buildsConfig[key]!==undefined){options[key]=buildsConfig[key]}}}catch(err){console.log("\x1B[33m\u26A0\uFE0F \u8BFB\u53D6 builds.config.js \u5931\u8D25: "+err.message+"\x1B[0m")}}// 2️⃣ 再尝试读取 build.json(会覆盖 builds.config.js 中的相同字段)
20
+ try{const fileData=fs.readFileSync(path.resolve(options["buildConfig"]),"utf8");try{const customOptions=JSON.parse(fileData);for(let key in customOptions){if(key==="defines")Object.assign(options.defines,customOptions[key]);else if(key==="compilation")Object.assign(options.compilation,customOptions[key]);else if(key==="adNetworkNames")Object.assign(options.adNetworkNames,customOptions[key]);else if(key==="google_play_url")options.googlePlayUrl=customOptions.google_play_url;// 覆盖 builds.config.js
21
+ else if(key==="app_store_url")options.appStoreUrl=customOptions.app_store_url;// 覆盖 builds.config.js
22
+ else{const possibleOption=possibleOptions.find(e=>e.alias===key||e.name===key);if(possibleOption&&(options[key]===undefined||options[key]===possibleOption.defaultValue)){options[key]=customOptions[key]}}}}catch(err){console.log("\x1B[31mBuild config parsing error: "+err.message+"\x1B[0m")}}catch(err){// build.json 不存在是正常的(使用 builds.config.js)
23
+ }// 环境变量覆盖(用于批量构建时避免竞态条件)
24
+ // 批量构建时通过 PLAYABLE_FILENAME 环境变量传递 filename,优先级最高
25
+ if(process.env.PLAYABLE_FILENAME){options.filename=process.env.PLAYABLE_FILENAME}console.log(`${name} v${version}`);console.log("Options:",options);exports.options=options;
@@ -0,0 +1,47 @@
1
+ "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
2
+ * AdikteevInjectorPlugin
3
+ *
4
+ * 在开发环境中注入 Adikteev MRAID mock 和占位符变量
5
+ *
6
+ * 官方文档: https://help.adikteev.com/hc/en-us/articles/10549028250130
7
+ *
8
+ * Adikteev 要求:
9
+ * 1. 使用 mraid.open() 打开目标 URL
10
+ * 2. 暴露 AK_CLICK_DESTINATION_URL 和 AK_CLICK_PIXEL_URL 占位符
11
+ * 3. 生产环境由 Adikteev CDN 提供 mraid.js
12
+ */class AdikteevInjectorPlugin{apply(compiler){compiler.hooks.compilation.tap("AdikteevInjectorPlugin",compilation=>{HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("AdikteevInjectorPlugin",(data,cb)=>{// 注入 MRAID mock (仅开发环境)
13
+ data.headTags.unshift({tagName:"script",innerHTML:`
14
+ // Adikteev MRAID Mock (Development Only)
15
+ if (typeof mraid === 'undefined') {
16
+ window.mraid = {
17
+ getState: function() {
18
+ console.log('[Adikteev Mock] mraid.getState() called');
19
+ return 'default';
20
+ },
21
+ open: function(url) {
22
+ console.log('[Adikteev Mock] mraid.open() called with URL:', url);
23
+ console.log('[Adikteev Mock] In production, this would open:', url);
24
+ // 开发环境下可选择性打开链接
25
+ // window.open(url, '_blank');
26
+ },
27
+ addEventListener: function(event, handler) {
28
+ console.log('[Adikteev Mock] addEventListener:', event);
29
+ },
30
+ removeEventListener: function(event, handler) {
31
+ console.log('[Adikteev Mock] removeEventListener:', event);
32
+ }
33
+ };
34
+ console.log('[Adikteev] MRAID mock initialized (development mode)');
35
+ }
36
+
37
+ // Adikteev 占位符变量 (生产环境会被 Adikteev 平台替换)
38
+ if (typeof AK_CLICK_DESTINATION_URL === 'undefined') {
39
+ window.AK_CLICK_DESTINATION_URL = "https://play.google.com/store/apps/details?id=com.example.app";
40
+ console.log('[Adikteev] AK_CLICK_DESTINATION_URL mock set');
41
+ }
42
+
43
+ if (typeof AK_CLICK_PIXEL_URL === 'undefined') {
44
+ window.AK_CLICK_PIXEL_URL = "";
45
+ console.log('[Adikteev] AK_CLICK_PIXEL_URL mock set (empty for dev)');
46
+ }
47
+ `,voidTag:false});cb(null,data)})})}}exports.AdikteevInjectorPlugin=AdikteevInjectorPlugin;
@@ -0,0 +1,7 @@
1
+ "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
2
+ * BigoAds SDK 注入插件
3
+ * 在 HTML 头部注入 BigoAds 专用 SDK 脚本
4
+ * 文档: https://www.bigoads.com/help/detail?id=143&moduleId=7&currentLan=CN
5
+ */class BigoAdsInjectorPlugin{apply(compiler){compiler.hooks.compilation.tap("BigoAdsInjectorPlugin",compilation=>{HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync("BigoAdsInjectorPlugin",(data,cb)=>{// BigoAds 要求必须引入此 SDK,不允许使用标准 mraid.js
6
+ const bigoAdsScript=`<script src="https://static-web.likeevideo.com/as/common-static/big-data/dsp-public/bgy-mraid-sdk.js"></script>`;// 在 <head> 标签后注入
7
+ data.html=data.html.replace("<head>",`<head>\n ${bigoAdsScript}`);cb(null,data)})})}}exports.BigoAdsInjectorPlugin=BigoAdsInjectorPlugin;
@@ -0,0 +1,5 @@
1
+ "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");const dapiSrc="function getScript(e,i){var n=document.createElement(\"script\");n.type=\"text/javascript\",n.async=!0,i&&(n.onload=i),n.src=e,document.head.appendChild(n)}function parseMessage(e){var i=e.data,n=i.indexOf(DOLLAR_PREFIX+RECEIVE_MSG_PREFIX);if(-1!==n){var t=i.slice(n+2);return getMessageParams(t)}return{}}function getMessageParams(e){var i,n=[],t=e.split(\"/\"),a=t.length;if(-1===e.indexOf(RECEIVE_MSG_PREFIX)){if(a>=2&&a%2===0)for(i=0;a>i;i+=2)n[t[i]]=t.length<i+1?null:decodeURIComponent(t[i+1])}else{var o=e.split(RECEIVE_MSG_PREFIX);void 0!==o[1]&&(n=JSON&&JSON.parse(o[1]))}return n}function getDapi(e){var i=parseMessage(e);if(!i||i.name===GET_DAPI_URL_MSG_NAME){var n=i.data;getScript(n,onDapiReceived)}}function invokeDapiListeners(){for(var e in dapiEventsPool)dapiEventsPool.hasOwnProperty(e)&&dapi.addEventListener(e,dapiEventsPool[e])}function onDapiReceived(){dapi=window.dapi,window.removeEventListener(\"message\",getDapi),invokeDapiListeners()}function init(){window.dapi.isDemoDapi&&(window.parent.postMessage(DOLLAR_PREFIX+SEND_MSG_PREFIX+JSON.stringify({state:\"getDapiUrl\"}),\"*\"),window.addEventListener(\"message\",getDapi,!1))}var DOLLAR_PREFIX=\"$$\",RECEIVE_MSG_PREFIX=\"DAPI_SERVICE:\",SEND_MSG_PREFIX=\"DAPI_AD:\",GET_DAPI_URL_MSG_NAME=\"connection.getDapiUrl\",dapiEventsPool={},dapi=window.dapi||{isReady:function(){return!1},addEventListener:function(e,i){dapiEventsPool[e]=i},removeEventListener:function(e){delete dapiEventsPool[e]},isDemoDapi:!0};init();";/**
2
+ * Webpack plugin that injects DAPI (Display Advertising Programming Interface) script
3
+ * into the HTML head. This enables communication with ad networks that use DAPI protocol.
4
+ * @implements {import('webpack').WebpackPluginInstance}
5
+ */class DAPIInjectorPlugin{apply(compiler){compiler.hooks.compilation.tap("DAPIInjectorPlugin",compilation=>{HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("DAPIInjectorPlugin",(data,callback)=>{data.headTags.splice(0,0,{tagName:"script",voidTag:false,meta:{plugin:"html-inline-script-webpack-plugin"},innerHTML:dapiSrc});callback(null,data)})})}}exports.DAPIInjectorPlugin=DAPIInjectorPlugin;
@@ -0,0 +1,6 @@
1
+ "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
2
+ * Webpack plugin that injects a custom debugger script into the HTML head
3
+ * @implements {import('webpack').WebpackPluginInstance}
4
+ */class DebuggerInjectionPlugin{/**
5
+ * @param {string} debuggerSrc - URL of the debugger script to inject
6
+ */constructor(debuggerSrc){this.debuggerSrc=debuggerSrc}apply(compiler){compiler.hooks.compilation.tap("DebuggerInjectionPlugin",compilation=>{HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("DebuggerInjectionPlugin",(data,callback)=>{data.headTags.splice(0,0,{tagName:"script",voidTag:false,meta:{plugin:"html-inline-script-webpack-plugin"},attributes:{src:this.debuggerSrc,type:"text/javascript"}});callback(null,data)})})}}exports.DebuggerInjectionPlugin=DebuggerInjectionPlugin;
@@ -0,0 +1,10 @@
1
+ "use strict";const HtmlWebpackPlugin=require("html-webpack-plugin");/**
2
+ * Webpack plugin that injects Google's ExitAPI script and required meta tags for Google Ads.
3
+ * Adds:
4
+ * - ExitAPI script for handling ad exits
5
+ * - Meta tag for ad size configuration
6
+ * - Meta tag for ad orientation support
7
+ * @implements {import('webpack').WebpackPluginInstance}
8
+ */class ExitAPIInjectorPlugin{/**
9
+ * @param {ORIENTATION} orientation - device orientation
10
+ */constructor(orientation){this.orientation=orientation}apply(compiler){compiler.hooks.compilation.tap("ExitAPIInjectorPlugin",compilation=>{HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("ExitAPIInjectorPlugin",(data,callback)=>{data.headTags.splice(0,0,{tagName:"script",voidTag:false,meta:{plugin:"html-inline-script-webpack-plugin"},attributes:{src:"https://tpc.googlesyndication.com/pagead/gadgets/html5/api/exitapi.js",type:"text/javascript"}});let width=320;let height=480;if(this.orientation==="square")width=480;else if(this.orientation==="landscape"){width=480;height=320}data.headTags.splice(0,0,{tagName:"meta",voidTag:true,meta:{plugin:"html-webpack-plugin"},attributes:{name:"ad.size",content:`width=${width},height=${height}`}});data.headTags.splice(0,0,{tagName:"meta",voidTag:true,meta:{plugin:"html-webpack-plugin"},attributes:{name:"ad.orientation",content:this.orientation==="both"||this.orientation==="square"?"portrait,landscape":this.orientation}});callback(null,data)})})}}exports.ExitAPIInjectorPlugin=ExitAPIInjectorPlugin;