@playcraft/devkit 1.0.30 → 1.0.31-beta.10
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.
- package/cli/commands/build.js +3 -11
- package/cli/commands/dev.js +1 -1
- package/cli/commands/pack.js +8 -6
- package/core/artifacts/emit.js +1 -0
- package/core/artifacts/fflate.js +1 -0
- package/core/artifacts/zip.js +1 -0
- package/core/batch-build.js +5 -7
- package/core/engines/cocos/clean-cache.js +14 -0
- package/core/engines/cocos/clean-cache.test.js +1 -0
- package/core/engines/cocos/cli-build.js +4 -0
- package/core/engines/cocos/cli.js +1 -0
- package/core/engines/cocos/constants.js +1 -0
- package/core/engines/cocos/creator-cli.js +1 -0
- package/core/engines/cocos/dev-server.js +5 -0
- package/core/engines/cocos/fs.js +1 -0
- package/core/engines/cocos/injects/3x/init.js +19 -0
- package/core/engines/cocos/injects/3x/main.js +2 -0
- package/core/engines/cocos/injects/libs/pako.js +1 -0
- package/core/engines/cocos/mcp-refresh.js +4 -0
- package/core/engines/cocos/merge.js +6 -0
- package/core/engines/cocos/pack.js +1 -0
- package/core/engines/cocos/preview-cache.js +5 -0
- package/core/engines/cocos/preview-cache.test.js +1 -0
- package/core/engines/cocos/preview-concat.js +8 -0
- package/core/engines/cocos/preview-concat.test.js +1 -0
- package/core/engines/cocos/preview-json-hook.js +1 -0
- package/core/engines/cocos/preview-json-pack.js +5 -0
- package/core/engines/cocos/preview-json-pack.test.js +2 -0
- package/core/engines/cocos/preview-proxy.js +1 -0
- package/core/engines/cocos/rebundle-preview.js +4 -0
- package/core/engines/cocos/strip-social-share.js +7 -0
- package/core/engines/cocos/theme-assets.js +3 -0
- package/core/engines/cocos/writable-cli.js +4 -0
- package/core/engines/cocos/writable-cli.test.js +1 -0
- package/core/engines/web/index.js +1 -0
- package/core/engines/web/vite.dev.js +32 -0
- package/core/engines/web/webpack.build.js +80 -0
- package/core/engines/web/webpack.common.js +11 -0
- package/core/engines/web/webpack.dev.js +16 -0
- package/core/index.d.ts +54 -0
- package/core/index.js +1 -1
- package/core/injectors/apply.js +1 -0
- package/core/injectors/check-channel-matrix.js +9 -0
- package/core/injectors/spec.js +107 -0
- package/core/options.js +6 -5
- package/core/utils/mergeOptions.js +1 -1
- package/core/utils/parseArgvOptions.js +2 -2
- package/core/validators/index.d.ts +4 -0
- package/core/validators/pre-build-checker.js +1 -1
- package/core/validators/tracking-validator.js +15 -15
- package/core/validators/tracking-validator.test.js +10 -0
- package/core/vite.dev.js +1 -32
- package/core/webpack.build.js +1 -81
- package/core/webpack.common.js +1 -11
- package/core/webpack.dev.js +1 -16
- package/package.json +4 -1
package/core/options.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
"use strict";const{parseArgvOptions,allowedAdProtocols,allowedAdNetworks,allowedPlaycraftOrientations,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:"is-channel-fold2zip",defaultValue:false,hasValue:false,description:"\u5F53\u542F\u7528\u65F6\uFF0C\u6784\u5EFA\u4EA7\u7269\u683C\u5F0F\u4E3A\u300C\u4E3B\u9898\u6587\u4EF6\u540D/\u6E20\u9053.zip\u300D"},{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:"playcraft-orientation",alias:"playcraftOrientation",defaultValue:["both"],hasValue:true,description:"\u5F53\u524D\u8981\u6253\u5305\u7684\u6A2A\u7AD6\u5C4F\u65B9\u5411\u7248\u672C\u5217\u8868\uFF0C\u53EF\u6307\u5B9A\u591A\u4E2A\uFF08\u9017\u53F7\u5206\u9694\uFF09\uFF0C\u53D6\u503C\uFF1Alandscape/portrait/both\u3002\u7528\u4E8E PLAYCRAFT_ORIENTATION define \u53CA window.playcraftOrientation \u8FD0\u884C\u65F6\u53D8\u91CF",parser:function(rawValue){const values=rawValue.split(",").map(v=>v.trim()).filter(Boolean);for(const value of values){if(!allowedPlaycraftOrientations.includes(value)){throw new Error(`--playcraft-orientation should have one of the value: ${allowedPlaycraftOrientations.join(", ")}`)}}return values}},{name:"playcraft-orientation-channel",alias:"playcraftOrientationChannel",defaultValue:[],hasValue:true,description:"\u54EA\u4E9B\u6E20\u9053\u9700\u8981\u6309 playcraftOrientation \u5206\u522B\u6253\u51FA\u591A\u4E2A\u65B9\u5411\u7248\u672C\u7684\u5305\uFF08\u9017\u53F7\u5206\u9694\u7684\u6E20\u9053\u540D\u5217\u8868\uFF09\u3002\u4E0D\u5728\u6B64\u5217\u8868\u4E2D\u7684\u6E20\u9053\u59CB\u7EC8\u6309 both\uFF08\u81EA\u9002\u5E94\uFF09\u5355\u4E00\u7248\u672C\u6253\u5305",parser:function(rawValue){return rawValue.split(",").map(v=>v.trim()).filter(Boolean)}},{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: 0=no minimize/obfuscate, 1=terser only, 2=obfuscator basic(default), 3=obfuscator+dead code, 4=maximum obfuscation",parser:function(rawValue){const value=+rawValue;if(isNaN(value)||value<0||value>4){throw new Error("--obfuscate-level should be a number between 0 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)}},{name:"exclude-compress-dirs",alias:"excludeCompressDirs",defaultValue:[],hasValue:true,description:"Directories to exclude from compression (comma-separated list of paths)",parser:function(rawValue){if(!rawValue)return[];return rawValue.split(",").map(dir=>dir.trim()).filter(Boolean)}}];/** @type {import('./index').CLIOptions} */const options=parseArgvOptions(possibleOptions);// Extract CLI explicit keys set, then remove it from options
|
|
3
|
-
const cliExplicitKeys=options._cliExplicitKeys||new Set;delete options._cliExplicitKeys;options.defines={};options.compilation={};options.adNetworkNames={}
|
|
1
|
+
"use strict";const{parseArgvOptions,allowedAdProtocols,allowedAdNetworks,allowedPlaycraftOrientations,allowedLanguages,allowedEngines}=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:"is-channel-fold2zip",defaultValue:false,hasValue:false,description:"\u5F53\u542F\u7528\u65F6\uFF0C\u6784\u5EFA\u4EA7\u7269\u683C\u5F0F\u4E3A\u300C\u4E3B\u9898\u6587\u4EF6\u540D/\u6E20\u9053.zip\u300D"},{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:"playcraft-orientation",alias:"playcraftOrientation",defaultValue:["both"],hasValue:true,description:"\u5F53\u524D\u8981\u6253\u5305\u7684\u6A2A\u7AD6\u5C4F\u65B9\u5411\u7248\u672C\u5217\u8868\uFF0C\u53EF\u6307\u5B9A\u591A\u4E2A\uFF08\u9017\u53F7\u5206\u9694\uFF09\uFF0C\u53D6\u503C\uFF1Alandscape/portrait/both\u3002\u7528\u4E8E PLAYCRAFT_ORIENTATION define \u53CA window.playcraftOrientation \u8FD0\u884C\u65F6\u53D8\u91CF",parser:function(rawValue){const values=rawValue.split(",").map(v=>v.trim()).filter(Boolean);for(const value of values){if(!allowedPlaycraftOrientations.includes(value)){throw new Error(`--playcraft-orientation should have one of the value: ${allowedPlaycraftOrientations.join(", ")}`)}}return values}},{name:"playcraft-orientation-channel",alias:"playcraftOrientationChannel",defaultValue:[],hasValue:true,description:"\u54EA\u4E9B\u6E20\u9053\u9700\u8981\u6309 playcraftOrientation \u5206\u522B\u6253\u51FA\u591A\u4E2A\u65B9\u5411\u7248\u672C\u7684\u5305\uFF08\u9017\u53F7\u5206\u9694\u7684\u6E20\u9053\u540D\u5217\u8868\uFF09\u3002\u4E0D\u5728\u6B64\u5217\u8868\u4E2D\u7684\u6E20\u9053\u59CB\u7EC8\u6309 both\uFF08\u81EA\u9002\u5E94\uFF09\u5355\u4E00\u7248\u672C\u6253\u5305",parser:function(rawValue){return rawValue.split(",").map(v=>v.trim()).filter(Boolean)}},{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: 0=no minimize/obfuscate, 1=terser only, 2=obfuscator basic(default), 3=obfuscator+dead code, 4=maximum obfuscation",parser:function(rawValue){const value=+rawValue;if(isNaN(value)||value<0||value>4){throw new Error("--obfuscate-level should be a number between 0 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)}},{name:"exclude-compress-dirs",alias:"excludeCompressDirs",defaultValue:[],hasValue:true,description:"Directories to exclude from compression (comma-separated list of paths)",parser:function(rawValue){if(!rawValue)return[];return rawValue.split(",").map(dir=>dir.trim()).filter(Boolean)}},{name:"engine",defaultValue:"",hasValue:true,description:"Playable engine: phaser, threejs, or cocos. Empty = original webpack pack",parser:function(rawValue){if(rawValue==null||rawValue==="")return"";if(!allowedEngines.includes(rawValue)){throw new Error(`--engine should have one of the value: ${allowedEngines.join(", ")} (omit or empty = webpack)`)}return rawValue}}];/** @type {import('./index').CLIOptions} */const options=parseArgvOptions(possibleOptions);// Extract CLI explicit keys set, then remove it from options
|
|
3
|
+
const cliExplicitKeys=options._cliExplicitKeys||new Set;delete options._cliExplicitKeys;options.defines={};options.compilation={};options.adNetworkNames={};options.cocos={buildConfig:"build-configs/web-mobile.json",// empty = derive from Creator buildConfig buildPath + outputName
|
|
4
|
+
webMobileDir:"",skipEngineBuild:false,creatorBin:"",generatedThemeFile:"assets/scripts/platform/theme.generated.ts",enableSplash:false,cleanCache:false};// ==================== 配置文件加载 ====================
|
|
4
5
|
//
|
|
5
6
|
// 优先级(从低到高):
|
|
6
7
|
// 1. builds.config.js (新配置文件,优先读取)
|
|
@@ -14,9 +15,9 @@ const cliExplicitKeys=options._cliExplicitKeys||new Set;delete options._cliExpli
|
|
|
14
15
|
const buildsConfigPath=path.resolve("builds.config.js");if(fs.existsSync(buildsConfigPath)){try{// 清除 require 缓存,确保读取最新配置
|
|
15
16
|
delete require.cache[buildsConfigPath];const buildsConfig=require(buildsConfigPath);// 读取商店链接配置(respect CLI args priority)
|
|
16
17
|
if(buildsConfig.googlePlayUrl&&!cliExplicitKeys.has("googlePlayUrl")){options.googlePlayUrl=buildsConfig.googlePlayUrl}if(buildsConfig.appStoreUrl&&!cliExplicitKeys.has("appStoreUrl")){options.appStoreUrl=buildsConfig.appStoreUrl}// 读取其他特殊配置
|
|
17
|
-
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)
|
|
18
|
+
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);if(buildsConfig.cocos&&typeof buildsConfig.cocos==="object"){Object.assign(options.cocos,buildsConfig.cocos)}// 读取所有通用配置字段(如果在 builds.config.js 中定义了)
|
|
18
19
|
for(const key in buildsConfig){// 跳过已处理的特殊字段和批量构建专用字段
|
|
19
|
-
if(["googlePlayUrl","appStoreUrl","defines","compilation","adNetworkNames","title","appName","projectCode","channels","themes","naming","build","hooks","cosConfig"].includes(key)){continue}// 查找对应的配置项定义
|
|
20
|
+
if(["googlePlayUrl","appStoreUrl","defines","compilation","adNetworkNames","title","appName","projectCode","channels","themes","naming","build","hooks","cosConfig","cocos"].includes(key)){continue}// 查找对应的配置项定义
|
|
20
21
|
const possibleOption=possibleOptions.find(e=>e.alias===key||e.name===key);if(possibleOption&&buildsConfig[key]!==undefined){// Only override if not explicitly set by CLI args
|
|
21
22
|
const optionKey=possibleOption.alias||possibleOption.name;if(!cliExplicitKeys.has(optionKey)){options[optionKey]=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 中的相同字段)
|
|
22
23
|
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"&&!cliExplicitKeys.has("googlePlayUrl"))options.googlePlayUrl=customOptions.google_play_url;else if(key==="app_store_url"&&!cliExplicitKeys.has("appStoreUrl"))options.appStoreUrl=customOptions.app_store_url;else{const possibleOption=possibleOptions.find(e=>e.alias===key||e.name===key);if(possibleOption){const optionKey=possibleOption.alias||possibleOption.name;// Only override if not explicitly set by CLI args
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* @param {import('../index').CLIOptions} options1 - First options object
|
|
4
4
|
* @param {Partial<import('../index').CLIOptions>} [options2] - Second options object
|
|
5
5
|
* @returns {import('../index').CLIOptions} Merged options object with combined defines and build properties
|
|
6
|
-
*/exports.mergeOptions=function mergeOptions(options1,options2){if(!options1)return{};if(!options2)return options1;const defines=Object.assign({},options1.defines,options2.defines);const build=Object.assign({},options1.build,options2.build);const compilation=Object.assign({},options1.compilation,options2.compilation);const adNetworkNames=Object.assign({},options1.adNetworkNames,options2.adNetworkNames);const options=Object.assign({},options1,options2);options.defines=defines;options.compilation=compilation;options.adNetworkNames=adNetworkNames;options.build=build;return options};
|
|
6
|
+
*/exports.mergeOptions=function mergeOptions(options1,options2){if(!options1)return{};if(!options2)return options1;const defines=Object.assign({},options1.defines,options2.defines);const build=Object.assign({},options1.build,options2.build);const compilation=Object.assign({},options1.compilation,options2.compilation);const adNetworkNames=Object.assign({},options1.adNetworkNames,options2.adNetworkNames);const cocos=Object.assign({},options1.cocos,options2.cocos);const options=Object.assign({},options1,options2);options.defines=defines;options.compilation=compilation;options.adNetworkNames=adNetworkNames;options.build=build;options.cocos=cocos;return options};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
"use strict";/** @type {string[]} List of supported advertising networks */const allowedAdNetworks=["preview","applovin","unity","google","ironsource","facebook","moloco","adcolony","mintegral","vungle","tapjoy","snapchat","tiktok","appreciate","chartboost","pangle","mytarget","liftoff","smadex","adikteev","bigabid","inmobi","bigoads","remerge","social_media"];/** @type {AD_PROTOCOL[]} List of supported advertising protocols */const allowedAdProtocols=["none","mraid","dapi"];/** @type {AD_NETWORK[]} List of advertising partners that support MRAID protocol */const mraidPartners=["ironsource","applovin","unity","appreciate","chartboost","mytarget","liftoff","adcolony","adikteev","bigabid","inmobi","remerge"];/** @type {LANGUAGE[]} */const allowedLanguages=["auto","en","es","zh","hi","ar","fr","de","ja","pt"];/** @type {('both'|'landscape'|'portrait')[]} playcraftOrientation 允许的横竖屏方向版本取值 */const allowedPlaycraftOrientations=["both","landscape","portrait"];/**
|
|
1
|
+
"use strict";/** @type {string[]} List of supported advertising networks */const allowedAdNetworks=["preview","applovin","unity","google","ironsource","facebook","moloco","adcolony","mintegral","vungle","tapjoy","snapchat","tiktok","appreciate","chartboost","pangle","mytarget","liftoff","smadex","adikteev","bigabid","inmobi","bigoads","remerge","social_media"];/** @type {AD_PROTOCOL[]} List of supported advertising protocols */const allowedAdProtocols=["none","mraid","dapi"];/** @type {AD_NETWORK[]} List of advertising partners that support MRAID protocol */const mraidPartners=["ironsource","applovin","unity","appreciate","chartboost","mytarget","liftoff","adcolony","adikteev","bigabid","inmobi","remerge"];/** @type {LANGUAGE[]} */const allowedLanguages=["auto","en","es","zh","hi","ar","fr","de","ja","pt"];/** @type {('both'|'landscape'|'portrait')[]} playcraftOrientation 允许的横竖屏方向版本取值 */const allowedPlaycraftOrientations=["both","landscape","portrait"];/** @type {('phaser'|'threejs'|'cocos')[]} */const allowedEngines=["phaser","threejs","cocos"];const WEB_ENGINES=["phaser","threejs"];function isWebEngine(engine){return!engine||WEB_ENGINES.includes(engine)}/**
|
|
2
2
|
* Parses command line arguments based on provided options configuration
|
|
3
3
|
* @param {import('../index').CLIOptionConfig[]} posiibleOptions - Array of possible options to parse
|
|
4
4
|
* @returns {Record<string, any>} Parsed options object with values from command line arguments
|
|
5
5
|
*/exports.parseArgvOptions=function parseArgvOptions(posiibleOptions){const argvOptions={};// Track which options were explicitly set via CLI args
|
|
6
6
|
const cliExplicitKeys=new Set;for(let possibleOption of posiibleOptions){if(possibleOption.defaultValue!==undefined)argvOptions[possibleOption.alias||possibleOption.name]=possibleOption.defaultValue}let adNetwork=null;for(let i=2;i<process.argv.length;i++){let key=process.argv[i];let inlineValue=null;// 支持 --key=value 格式
|
|
7
7
|
if(key.includes("=")){const eqIndex=key.indexOf("=");inlineValue=key.slice(eqIndex+1);key=key.slice(0,eqIndex)}const allowedOption=posiibleOptions.find(e=>"--"+e.name===key);if(allowedOption){let value=true;if(allowedOption.hasValue){// 优先使用内联值(--key=value),否则使用下一个参数
|
|
8
|
-
value=inlineValue!==null?inlineValue:process.argv[++i];if(allowedOption.parser)value=allowedOption.parser(value)}const optionKey=allowedOption.alias||allowedOption.name;argvOptions[optionKey]=value;cliExplicitKeys.add(optionKey)}else if(!adNetwork)adNetwork=key}if(posiibleOptions.find(e=>e.name==="network")){if(adNetwork&&allowedAdNetworks.includes(adNetwork)){argvOptions["network"]=adNetwork}if(posiibleOptions.find(e=>e.name==="protocol")){if(mraidPartners.includes(argvOptions["network"])&&argvOptions["protocol"]==="none"){argvOptions["protocol"]="mraid"}}}argvOptions._cliExplicitKeys=cliExplicitKeys;return argvOptions};exports.allowedAdNetworks=allowedAdNetworks;exports.allowedAdProtocols=allowedAdProtocols;exports.mraidPartners=mraidPartners;exports.allowedLanguages=allowedLanguages;exports.allowedPlaycraftOrientations=allowedPlaycraftOrientations;
|
|
8
|
+
value=inlineValue!==null?inlineValue:process.argv[++i];if(allowedOption.parser)value=allowedOption.parser(value)}const optionKey=allowedOption.alias||allowedOption.name;argvOptions[optionKey]=value;cliExplicitKeys.add(optionKey)}else if(!adNetwork)adNetwork=key}if(posiibleOptions.find(e=>e.name==="network")){if(adNetwork&&allowedAdNetworks.includes(adNetwork)){argvOptions["network"]=adNetwork}if(posiibleOptions.find(e=>e.name==="protocol")){if(mraidPartners.includes(argvOptions["network"])&&argvOptions["protocol"]==="none"){argvOptions["protocol"]="mraid"}}}argvOptions._cliExplicitKeys=cliExplicitKeys;return argvOptions};exports.allowedAdNetworks=allowedAdNetworks;exports.allowedAdProtocols=allowedAdProtocols;exports.mraidPartners=mraidPartners;exports.allowedLanguages=allowedLanguages;exports.allowedPlaycraftOrientations=allowedPlaycraftOrientations;exports.allowedEngines=allowedEngines;exports.WEB_ENGINES=WEB_ENGINES;exports.isWebEngine=isWebEngine;
|
|
@@ -52,6 +52,10 @@ export interface ValidationOptions {
|
|
|
52
52
|
extensions?: string[];
|
|
53
53
|
/** 排除的目录列表 */
|
|
54
54
|
excludeDirs?: string[];
|
|
55
|
+
/** 引擎:cocos / phaser / threejs,决定扫描目录 */
|
|
56
|
+
engine?: string;
|
|
57
|
+
/** 覆盖默认扫描目录(相对项目根) */
|
|
58
|
+
scanDirs?: string[];
|
|
55
59
|
/** 是否输出详细信息 */
|
|
56
60
|
verbose?: boolean;
|
|
57
61
|
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// ============================================================
|
|
23
23
|
/**
|
|
24
24
|
* 埋点验证检查器
|
|
25
|
-
*/checkers.set("tracking",{name:"\u57CB\u70B9\u9A8C\u8BC1",description:"\u9A8C\u8BC1\u9879\u76EE\u4E2D\u662F\u5426\u6B63\u786E\u8C03\u7528\u4E86\u6240\u6709\u5FC5\u987B\u7684\u57CB\u70B9\u65B9\u6CD5",enabled:true,validator:(projectRoot,options={})=>{const result=validateTracking(projectRoot,{verbose:options.verbose||false,extensions:options.extensions,excludeDirs:options.excludeDirs});return{success:result.success,message:result.success?"\u2705 \u57CB\u70B9\u9A8C\u8BC1\u901A\u8FC7":`❌ 埋点验证失败:缺少 ${result.errors.length} 个必须的埋点方法`,data:result}}});/**
|
|
25
|
+
*/checkers.set("tracking",{name:"\u57CB\u70B9\u9A8C\u8BC1",description:"\u9A8C\u8BC1\u9879\u76EE\u4E2D\u662F\u5426\u6B63\u786E\u8C03\u7528\u4E86\u6240\u6709\u5FC5\u987B\u7684\u57CB\u70B9\u65B9\u6CD5",enabled:true,validator:(projectRoot,options={})=>{const result=validateTracking(projectRoot,{verbose:options.verbose||false,extensions:options.extensions,excludeDirs:options.excludeDirs,engine:options.engine,scanDirs:options.scanDirs});return{success:result.success,message:result.success?"\u2705 \u57CB\u70B9\u9A8C\u8BC1\u901A\u8FC7":`❌ 埋点验证失败:缺少 ${result.errors.length} 个必须的埋点方法`,data:result}}});/**
|
|
26
26
|
* 主题名称检查器
|
|
27
27
|
* 校验本次构建涉及的主题名称长度是否超过限制(不超过 50 个字符)。
|
|
28
28
|
* 主题名称过长可能导致下游文件名 / 目录名 / COS Key 超出系统或第三方平台限制。
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
*/const TRACKING_METHODS=[{name:"onPlaycraftTrackingInit",required:true,description:"SDK \u521D\u59CB\u5316\u5F00\u59CB",channels:["Bigabid:mraid_viewable","InMobi:Ad_Load_Start","AppLovin:LOADING"]},{name:"onPlaycraftTrackingLoading",required:true,description:"\u6E38\u620F\u8D44\u6E90\u52A0\u8F7D\u4E2D",channels:["Bigabid:game_viewable","InMobi:Ad_Viewable","AppLovin:LOADED"]},{name:"onPlaycraftTrackingLoaded",required:true,description:"\u4E3B\u573A\u666F\u5C31\u7EEA\uFF0C\u7528\u6237\u53EF\u4EA4\u4E92",channels:["AppLovin:DISPLAYED"]},{name:"onPlaycraftChallengeStart",required:true,description:"\u6311\u6218\u5F00\u59CB\uFF08\u9996\u6B21\u7528\u6237\u4EA4\u4E92\uFF09",channels:["Bigabid:engagement","InMobi:First_Engagement","AppLovin:CHALLENGE_STARTED"]},{name:"onPlaycraftChallengeProgress",required:false,description:"\u8FDB\u5EA6\u767E\u5206\u6BD4\u8FFD\u8E2A\uFF08\u53EF\u9009\uFF09",channels:["AppLovin:CHALLENGE_PASS_*"]},{name:"onPlaycraftChallengeSuccess",required:true,description:"\u6E38\u620F\u6311\u6218\u6210\u529F",channels:["AppLovin:CHALLENGE_SOLVED","Bigabid:complete","InMobi:Gameplay_Complete"]},{name:"onPlaycraftChallengeFinish",required:false,description:"\u6E38\u620F\u7ED3\u675F\uFF08\u53EF\u9009\uFF09",channels:["Bigabid:complete","InMobi:Gameplay_Complete","AppLovin:ENDCARD_SHOWN"]},{name:"onPlaycraftInstall",required:true,description:"\u70B9\u51FB\u5B89\u88C5",channels:["Bigabid:click","InMobi:DSP_Click","AppLovin:CTA_CLICKED"]},{name:"onPlaycraftRetry",required:false,description:"\u91CD\u8BD5\u6E38\u620F\uFF08\u53EF\u9009\uFF09",channels:["AppLovin:CHALLENGE_RETRY"]},{name:"onPlaycraftChallengeFailed",required:true,description:"\u6E38\u620F\u6311\u6218\u5931\u8D25",channels:["AppLovin:CHALLENGE_FAILED"]}];/**
|
|
15
15
|
* SDK 静态方法映射到 tracking 方法
|
|
16
16
|
* 这些 SDK 方法内部会调用对应的 tracking 方法
|
|
17
|
-
*/const SDK_TO_TRACKING_MAP={"playcraftInit":"onPlaycraftTrackingInit","playcraftStart":"onPlaycraftTrackingLoading","playcraftInteractive":"onPlaycraftTrackingLoaded","playcraftChallengeStart":"onPlaycraftChallengeStart","recordPlaycraftChallengeProgress":"onPlaycraftChallengeProgress","playcraftRecordSuccess":"onPlaycraftChallengeSuccess","playcraftFinish":"onPlaycraftChallengeFinish","playcraftInstall":"onPlaycraftInstall","playcraftRetry":"onPlaycraftRetry","playcraftRecordFailed":"onPlaycraftChallengeFailed"}
|
|
17
|
+
*/const SDK_TO_TRACKING_MAP={"playcraftInit":"onPlaycraftTrackingInit","playcraftStart":"onPlaycraftTrackingLoading","playcraftInteractive":"onPlaycraftTrackingLoaded","playcraftChallengeStart":"onPlaycraftChallengeStart","recordPlaycraftChallengeProgress":"onPlaycraftChallengeProgress","playcraftRecordSuccess":"onPlaycraftChallengeSuccess","playcraftFinish":"onPlaycraftChallengeFinish","playcraftInstall":"onPlaycraftInstall","playcraftRetry":"onPlaycraftRetry","playcraftRecordFailed":"onPlaycraftChallengeFailed"};/** Phaser / Three 游戏逻辑在 src;Cocos 在 assets/scripts。不要扫 library/temp。 */const ENGINE_SCAN_ROOTS={cocos:["assets/scripts"],phaser:["src"],threejs:["src"]};const DEFAULT_EXCLUDE_DIRS=["node_modules","dist","build",".git",".claude","library","temp","profiles","native","local","dist-all"];function detectProjectEngine(projectRoot,explicit){if(explicit)return String(explicit).toLowerCase();try{const configPath=path.join(projectRoot,"builds.config.js");if(fs.existsSync(configPath)){delete require.cache[require.resolve(configPath)];const cfg=require(configPath);if(cfg&&cfg.engine)return String(cfg.engine).toLowerCase()}}catch{// ignore invalid builds.config.js
|
|
18
|
+
}if(fs.existsSync(path.join(projectRoot,"settings/cocos.config.json")))return"cocos";if(fs.existsSync(path.join(projectRoot,"assets/scripts"))&&fs.existsSync(path.join(projectRoot,"assets/scenes"))){return"cocos"}return""}function resolveScanRoots(projectRoot,engine){const rels=ENGINE_SCAN_ROOTS[engine]||ENGINE_SCAN_ROOTS.phaser;const roots=rels.map(rel=>path.join(projectRoot,rel)).filter(dir=>fs.existsSync(dir));return roots.length?roots:[projectRoot]}function toRelScanRoots(projectRoot,roots){return roots.map(dir=>{const rel=path.relative(projectRoot,dir);return rel||"."})}// ============================================================
|
|
18
19
|
// 文件扫描工具
|
|
19
20
|
// ============================================================
|
|
20
21
|
/**
|
|
@@ -23,9 +24,9 @@
|
|
|
23
24
|
* @param {string[]} extensions - 文件扩展名列表
|
|
24
25
|
* @param {string[]} excludeDirs - 排除的目录名列表
|
|
25
26
|
* @returns {string[]} 文件路径列表
|
|
26
|
-
*/function scanSourceFiles(dir,extensions=[".ts",".tsx",".js",".jsx"],excludeDirs=
|
|
27
|
+
*/function scanSourceFiles(dir,extensions=[".ts",".tsx",".js",".jsx"],excludeDirs=DEFAULT_EXCLUDE_DIRS){const files=[];function scan(currentDir){if(!fs.existsSync(currentDir))return;const entries=fs.readdirSync(currentDir,{withFileTypes:true});for(const entry of entries){const fullPath=path.join(currentDir,entry.name);if(entry.isDirectory()){// 跳过排除的目录
|
|
27
28
|
if(excludeDirs.includes(entry.name))continue;scan(fullPath)}else if(entry.isFile()){// 检查文件扩展名
|
|
28
|
-
const ext=path.extname(entry.name);if(extensions.includes(ext)){files.push(fullPath)}}}}scan(dir);return files}/**
|
|
29
|
+
const ext=path.extname(entry.name);if(extensions.includes(ext)){files.push(fullPath)}}}}scan(dir);return files}function scanSourceFilesFromRoots(roots,extensions,excludeDirs){const files=[];const seen=new Set;for(const root of roots){for(const file of scanSourceFiles(root,extensions,excludeDirs)){if(seen.has(file))continue;seen.add(file);files.push(file)}}return files}/**
|
|
29
30
|
* 读取文件内容(带错误处理)
|
|
30
31
|
* @param {string} filePath - 文件路径
|
|
31
32
|
* @returns {string|null} 文件内容或 null
|
|
@@ -58,11 +59,9 @@ new RegExp(`\\["${sdkMethod}"\\]\\s*\\(`)];return patterns.some(pattern=>pattern
|
|
|
58
59
|
* 检测是否导入了 @playcraft/ads-tracking
|
|
59
60
|
* @param {string} content - 文件内容
|
|
60
61
|
* @returns {boolean} 是否导入了 tracking
|
|
61
|
-
*/function hasTrackingImport(content){return /from\s+['"]@playcraft\/ads-tracking['"]/.test(content)||/require\s*\(\s*['"]@playcraft\/ads-tracking['"]\s*\)/.test(content)}/**
|
|
62
|
-
* 检测是否导入了 @playcraft/adsdk
|
|
63
|
-
|
|
64
|
-
* @returns {boolean} 是否导入了 SDK
|
|
65
|
-
*/function hasSDKImport(content){return /from\s+['"]@playcraft\/adsdk['"]/.test(content)||/require\s*\(\s*['"]@playcraft\/adsdk['"]\s*\)/.test(content)}// ============================================================
|
|
62
|
+
*/function hasTrackingImport(content){return /from\s+['"]@playcraft\/ads-tracking(?:\/[^'"]*)?['"]/.test(content)||/require\s*\(\s*['"]@playcraft\/ads-tracking(?:\/[^'"]*)?['"]\s*\)/.test(content)}/**
|
|
63
|
+
* 检测是否导入了 @playcraft/adsdk(含 /cjs 等子路径,以及项目内 playcraft-adsdk 封装)
|
|
64
|
+
*/function hasSDKImport(content){return /from\s+['"]@playcraft\/adsdk(?:\/[^'"]*)?['"]/.test(content)||/require\s*\(\s*['"]@playcraft\/adsdk(?:\/[^'"]*)?['"]\s*\)/.test(content)||/from\s+['"][^'"]*playcraft-adsdk['"]/.test(content)||/require\s*\(\s*['"][^'"]*playcraft-adsdk['"]\s*\)/.test(content)}function hasAnySdkTrackingCall(content){return Object.keys(SDK_TO_TRACKING_MAP).some(sdkMethod=>detectSDKCall(content,sdkMethod))}// ============================================================
|
|
66
65
|
// 验证核心逻辑
|
|
67
66
|
// ============================================================
|
|
68
67
|
/**
|
|
@@ -80,13 +79,14 @@ new RegExp(`\\["${sdkMethod}"\\]\\s*\\(`)];return patterns.some(pattern=>pattern
|
|
|
80
79
|
* @param {Object} [options] - 验证选项
|
|
81
80
|
* @param {string[]} [options.extensions] - 文件扩展名
|
|
82
81
|
* @param {string[]} [options.excludeDirs] - 排除的目录
|
|
82
|
+
* @param {string} [options.engine] - 引擎:cocos / phaser / threejs
|
|
83
|
+
* @param {string[]} [options.scanDirs] - 覆盖默认扫描目录(相对项目根)
|
|
83
84
|
* @param {boolean} [options.verbose] - 是否输出详细信息
|
|
84
85
|
* @returns {ValidationResult} 验证结果
|
|
85
|
-
*/function validateTracking(projectRoot,options={}){const{extensions=[".ts",".tsx",".js",".jsx"],excludeDirs=
|
|
86
|
-
if(verbose)console.log(`\n[扫描]
|
|
86
|
+
*/function validateTracking(projectRoot,options={}){const{extensions=[".ts",".tsx",".js",".jsx"],excludeDirs=DEFAULT_EXCLUDE_DIRS,verbose=false}=options;const engine=detectProjectEngine(projectRoot,options.engine);const scanRoots=Array.isArray(options.scanDirs)&&options.scanDirs.length?options.scanDirs.map(dir=>path.resolve(projectRoot,dir)).filter(dir=>fs.existsSync(dir)):resolveScanRoots(projectRoot,engine);const result={success:false,integrationType:"none",methods:[],errors:[],warnings:[],stats:{totalFiles:0,scannedFiles:0,hasTrackingImport:false,hasSDKImport:false,engine,scanRoots:toRelScanRoots(projectRoot,scanRoots)}};// 1. 按引擎扫描源码目录,避免 Cocos library/temp 把检测带偏
|
|
87
|
+
if(verbose){console.log(`\n[扫描] 项目: ${projectRoot}`);console.log(`[扫描] 引擎: ${engine||"(default/src)"}`);console.log(`[扫描] 目录: ${result.stats.scanRoots.join(", ")}`)}const files=scanSourceFilesFromRoots(scanRoots,extensions,excludeDirs);result.stats.totalFiles=files.length;if(files.length===0){result.errors.push("\u672A\u627E\u5230\u4EFB\u4F55\u6E90\u7801\u6587\u4EF6");return result}if(verbose)console.log(`[扫描] 找到 ${files.length} 个源码文件\n`);// 2. 读取所有文件内容并合并(用于全局检测)
|
|
87
88
|
let allContent="";for(const file of files){const content=readFileContent(file);if(content){allContent+=content+"\n";result.stats.scannedFiles++}}// 3. 判断集成类型
|
|
88
|
-
result.stats.hasTrackingImport=hasTrackingImport(allContent);result.stats.hasSDKImport=hasSDKImport(allContent);if(result.stats.
|
|
89
|
-
}else if(result.stats.hasSDKImport){result.integrationType="sdk"}else if(result.stats.hasTrackingImport){result.integrationType="tracking"}else{result.errors.push("\u672A\u68C0\u6D4B\u5230 @playcraft/adsdk \u6216 @playcraft/ads-tracking \u7684\u5BFC\u5165");return result}if(verbose){console.log(`[集成类型] ${result.integrationType==="sdk"?"SDK \u96C6\u6210\uFF08\u901A\u8FC7 sdk \u9759\u6001\u65B9\u6CD5\u8C03\u7528\uFF09":"\u76F4\u63A5 tracking \u96C6\u6210"}`);console.log(`[导入检测] SDK: ${result.stats.hasSDKImport?"\u2713":"\u2717"} | Tracking: ${result.stats.hasTrackingImport?"\u2713":"\u2717"}\n`)}// 4. 检测每个埋点方法
|
|
89
|
+
result.stats.hasTrackingImport=hasTrackingImport(allContent);result.stats.hasSDKImport=hasSDKImport(allContent);if(result.stats.hasSDKImport||hasAnySdkTrackingCall(allContent)){result.integrationType="sdk"}else if(result.stats.hasTrackingImport){result.integrationType="tracking"}else{result.errors.push("\u672A\u68C0\u6D4B\u5230 @playcraft/adsdk \u6216 @playcraft/ads-tracking \u7684\u5BFC\u5165");return result}if(verbose){console.log(`[集成类型] ${result.integrationType==="sdk"?"SDK \u96C6\u6210\uFF08\u901A\u8FC7 sdk \u9759\u6001\u65B9\u6CD5\u8C03\u7528\uFF09":"\u76F4\u63A5 tracking \u96C6\u6210"}`);console.log(`[导入检测] SDK: ${result.stats.hasSDKImport?"\u2713":"\u2717"} | Tracking: ${result.stats.hasTrackingImport?"\u2713":"\u2717"}\n`)}// 4. 检测每个埋点方法
|
|
90
90
|
for(const method of TRACKING_METHODS){let found=false;if(result.integrationType==="sdk"){// SDK 集成:检测 SDK 方法调用
|
|
91
91
|
const sdkMethod=Object.keys(SDK_TO_TRACKING_MAP).find(key=>SDK_TO_TRACKING_MAP[key]===method.name);if(sdkMethod){found=detectSDKCall(allContent,sdkMethod)}}else if(result.integrationType==="tracking"){// 直接 tracking 集成:检测 tracking 方法调用
|
|
92
92
|
found=detectDirectTrackingCall(allContent,method.name)}result.methods.push({method:method.name,found,required:method.required,description:method.description});// 必须方法未找到 -> 错误
|
|
@@ -100,7 +100,7 @@ result.success=result.errors.length===0;return result}// =======================
|
|
|
100
100
|
* @param {ValidationResult} result - 验证结果
|
|
101
101
|
* @param {boolean} [showDetails=true] - 是否显示详细信息
|
|
102
102
|
*/function printValidationResult(result,showDetails=true){console.log("\n"+"=".repeat(60));console.log("\uD83D\uDCCA \u57CB\u70B9\u9A8C\u8BC1\u62A5\u544A");console.log("=".repeat(60));// 统计信息
|
|
103
|
-
console.log(`\n📁 文件统计: 扫描 ${result.stats.scannedFiles}/${result.stats.totalFiles} 个文件`);console.log(`🔗 集成类型: ${result.integrationType==="sdk"?"SDK \u96C6\u6210":result.integrationType==="tracking"?"Tracking \u76F4\u63A5\u96C6\u6210":"\u672A\u68C0\u6D4B\u5230"}`);// 方法检测结果
|
|
103
|
+
console.log(`\n📁 文件统计: 扫描 ${result.stats.scannedFiles}/${result.stats.totalFiles} 个文件`);if(result.stats.engine||result.stats.scanRoots&&result.stats.scanRoots.length){console.log(`🧭 引擎: ${result.stats.engine||"default"} | 目录: ${(result.stats.scanRoots||[]).join(", ")||"."}`)}console.log(`🔗 集成类型: ${result.integrationType==="sdk"?"SDK \u96C6\u6210":result.integrationType==="tracking"?"Tracking \u76F4\u63A5\u96C6\u6210":"\u672A\u68C0\u6D4B\u5230"}`);// 方法检测结果
|
|
104
104
|
if(showDetails){console.log("\n\uD83D\uDCCB \u65B9\u6CD5\u68C0\u6D4B\u660E\u7EC6:");console.log("-".repeat(60));const requiredMethods=result.methods.filter(m=>m.required);const optionalMethods=result.methods.filter(m=>!m.required);console.log("\n\u5FC5\u987B\u65B9\u6CD5:");for(const m of requiredMethods){const status=m.found?"\u2705":"\u274C";console.log(` ${status} ${m.method.padEnd(35)} ${m.found?"\u5DF2\u8C03\u7528":"\u672A\u8C03\u7528"}`)}console.log("\n\u53EF\u9009\u65B9\u6CD5:");for(const m of optionalMethods){const status=m.found?"\u2705":"\u26A0\uFE0F ";console.log(` ${status} ${m.method.padEnd(35)} ${m.found?"\u5DF2\u8C03\u7528":"\u672A\u8C03\u7528"}`)}}// 错误信息
|
|
105
105
|
if(result.errors.length>0){console.log("\n\u274C \u9519\u8BEF:");result.errors.forEach(err=>console.log(` ${err}`))}// 警告信息
|
|
106
106
|
if(result.warnings.length>0){console.log("\n\u26A0\uFE0F \u8B66\u544A:");result.warnings.forEach(warn=>console.log(` ${warn}`))}// 最终结果
|
|
@@ -109,5 +109,5 @@ console.log("\n"+"=".repeat(60));if(result.success){console.log("\u2705 \u9A8C\u
|
|
|
109
109
|
// ============================================================
|
|
110
110
|
module.exports={// 核心验证函数
|
|
111
111
|
validateTracking,printValidationResult,// 配置数据(供外部使用)
|
|
112
|
-
TRACKING_METHODS,SDK_TO_TRACKING_MAP,// 工具函数(供外部使用)
|
|
113
|
-
scanSourceFiles,detectDirectTrackingCall,detectSDKCall,hasTrackingImport,hasSDKImport};
|
|
112
|
+
TRACKING_METHODS,SDK_TO_TRACKING_MAP,ENGINE_SCAN_ROOTS,DEFAULT_EXCLUDE_DIRS,// 工具函数(供外部使用)
|
|
113
|
+
detectProjectEngine,resolveScanRoots,scanSourceFiles,detectDirectTrackingCall,detectSDKCall,hasTrackingImport,hasSDKImport};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";const{describe,it,before,after}=require("node:test");const assert=require("node:assert/strict");const fs=require("fs");const os=require("os");const path=require("path");const{validateTracking,detectProjectEngine,resolveScanRoots,hasSDKImport}=require("./tracking-validator.js");const SDK_SOURCE=`
|
|
2
|
+
import { sdk } from '@playcraft/adsdk/cjs';
|
|
3
|
+
sdk.playcraftInit(() => {});
|
|
4
|
+
sdk.playcraftStart();
|
|
5
|
+
sdk.playcraftInteractive();
|
|
6
|
+
sdk.playcraftChallengeStart();
|
|
7
|
+
sdk.playcraftRecordSuccess();
|
|
8
|
+
sdk.playcraftRecordFailed();
|
|
9
|
+
sdk.playcraftInstall();
|
|
10
|
+
`;describe("tracking validator engine scan roots",()=>{let root;before(()=>{root=fs.mkdtempSync(path.join(os.tmpdir(),"tracking-scan-"));fs.mkdirSync(path.join(root,"assets/scripts/game"),{recursive:true});fs.mkdirSync(path.join(root,"assets/scenes"),{recursive:true});fs.mkdirSync(path.join(root,"library/import"),{recursive:true});fs.mkdirSync(path.join(root,"src"),{recursive:true});fs.writeFileSync(path.join(root,"builds.config.js"),"module.exports = { engine: 'cocos' };\n");fs.writeFileSync(path.join(root,"assets/scripts/game/GameRoot.ts"),SDK_SOURCE);fs.writeFileSync(path.join(root,"library/import/noise.js"),"import { tracking } from '@playcraft/ads-tracking';\ntracking.onPlaycraftTrackingInit();\n");fs.writeFileSync(path.join(root,"src/index.ts"),"export const unused = 1;\n")});after(()=>{fs.rmSync(root,{recursive:true,force:true})});it("treats @playcraft/adsdk/cjs as an SDK import",()=>{assert.equal(hasSDKImport("import * as adsdk from '@playcraft/adsdk/cjs';"),true);assert.equal(hasSDKImport("import { sdk } from '@playcraft/adsdk';"),true)});it("detects cocos from builds.config.js and scans assets/scripts only",()=>{assert.equal(detectProjectEngine(root),"cocos");const roots=resolveScanRoots(root,"cocos").map(dir=>path.relative(root,dir));assert.deepEqual(roots,["assets/scripts"])});it("passes required tracking checks for a cocos SDK wrapper project",()=>{const result=validateTracking(root,{engine:"cocos"});assert.equal(result.stats.engine,"cocos");assert.deepEqual(result.stats.scanRoots,["assets/scripts"]);assert.equal(result.integrationType,"sdk");assert.equal(result.success,true,result.errors.join("\n"));assert.equal(result.stats.hasSDKImport,true)});it("does not let library ads-tracking imports flip the integration type",()=>{const result=validateTracking(root,{engine:"cocos"});assert.equal(result.stats.hasTrackingImport,false);assert.ok(result.stats.scannedFiles<10)});it("scans src for phaser",()=>{const phaserRoot=fs.mkdtempSync(path.join(os.tmpdir(),"tracking-phaser-"));fs.mkdirSync(path.join(phaserRoot,"src"),{recursive:true});fs.mkdirSync(path.join(phaserRoot,"library"),{recursive:true});fs.writeFileSync(path.join(phaserRoot,"src/main.ts"),SDK_SOURCE.replace("/cjs",""));fs.writeFileSync(path.join(phaserRoot,"library/noise.js"),"import { tracking } from '@playcraft/ads-tracking';\n");try{const result=validateTracking(phaserRoot,{engine:"phaser"});assert.deepEqual(result.stats.scanRoots,["src"]);assert.equal(result.integrationType,"sdk");assert.equal(result.success,true,result.errors.join("\n"))}finally{fs.rmSync(phaserRoot,{recursive:true,force:true})}})});
|
package/core/vite.dev.js
CHANGED
|
@@ -1,32 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/** JSON5 文件加载插件 */function json5Plugin(){return{name:"vite-plugin-json5",transform(code,id){if(id.endsWith(".json5")){const JSON5=require("json5");const raw=fs.readFileSync(id,"utf-8");const parsed=JSON5.parse(raw);return{code:`export default ${JSON.stringify(parsed)};`,map:null}}}}}/** 资源内联插件(模拟 webpack 的 asset/inline 行为) */function assetInlinePlugin(){return{name:"vite-plugin-asset-inline",enforce:"pre",transform(code,id){// .atlas 文件 -> base64 data URL
|
|
3
|
-
if(id.endsWith(".atlas")){const content=fs.readFileSync(id);const base64=content.toString("base64");return{code:`export default "data:text/atlas;base64,${base64}";`,map:null}}// .fnt 文件 -> base64 data URL
|
|
4
|
-
if(id.endsWith(".fnt")){const content=fs.readFileSync(id);const base64=content.toString("base64");return{code:`export default "data:text/plain;base64,${base64}";`,map:null}}// .xml 文件 -> 原始文本
|
|
5
|
-
if(id.endsWith(".xml")){const raw=fs.readFileSync(id,"utf-8");return{code:`export default ${JSON.stringify(raw)};`,map:null}}}}}/** 字体注入插件(dev 模式:读取字体文件 base64 内联到 index.html) */function fontFamilyPlugin(){let _themeName=null;let _themeConfig=null;// 检测当前主题名(与 build.js 中 detectCurrentTheme 逻辑一致)
|
|
6
|
-
function detectTheme(){const root=process.cwd();const buildsConfigPath=path.join(root,"builds.config.js");let config={sourceDir:"src/theme",entryFile:"src/theme/index.ts"};if(fs.existsSync(buildsConfigPath)){try{delete require.cache[require.resolve(buildsConfigPath)];const buildsConfig=require(buildsConfigPath);if(buildsConfig.themes){config={...config,...buildsConfig.themes}}}catch(_){/* ignore */}}let name=null;const entryPath=path.join(root,config.entryFile);if(fs.existsSync(entryPath)){const content=fs.readFileSync(entryPath,"utf8");const match=content.match(/from\s+['"]\.\/([^'"]+)['"]/);if(match)name=match[1]}return{themeName:name,themeConfig:config}}return{name:"vite-plugin-font-family",apply:"serve",transformIndexHtml:{order:"pre",handler(){if(_themeName===null){const detected=detectTheme();_themeName=detected.themeName;_themeConfig=detected.themeConfig}if(!_themeName)return"";const css=getOrGenerateFontCSS({projectRoot:process.cwd(),themeName:_themeName,themeConfig:_themeConfig});return css?[{tag:"style",attrs:{"data-playable-fonts":""},children:css.replace(/<\/?style[^>]*>/g,""),injectTo:"head"}]:[]}}}}// ── makeViteDevConfig / runViteDev ──
|
|
7
|
-
/**
|
|
8
|
-
* 创建 Vite 开发配置
|
|
9
|
-
* @param {Partial<import('./index').CLIOptions>} [customOptions] - 自定义选项,会与默认选项合并
|
|
10
|
-
* @param {Record<string, any>} [customDefines] - 额外的 define 常量
|
|
11
|
-
* @param {import('vite').UserConfig} [viteCustomConfig] - 自定义 Vite 配置,会与默认配置合并
|
|
12
|
-
* @returns {import('vite').UserConfig} 最终的 Vite 开发配置
|
|
13
|
-
*/function makeViteDevConfig(customOptions,customDefines,viteCustomConfig){const devOptions=mergeOptions(options,customOptions);logOptions(devOptions);customDefines=customDefines||{};viteCustomConfig=viteCustomConfig||{};// 构建 define 常量(复用 webpack 的 buildDefines 逻辑)
|
|
14
|
-
const defines={...buildDefines(),...devOptions.defines,__DEV__:JSON.stringify(devOptions["dev"]===undefined?true:devOptions["dev"]),...customDefines};// 解析 tsconfig/jsconfig 路径别名
|
|
15
|
-
const tsConfigPath=path.resolve(devOptions.tsConfig||"tsconfig.json");const jsConfigPath=path.resolve(devOptions.jsConfig||"jsconfig.json");let aliasConfig={};// 尝试从 tsconfig/jsconfig 中读取 paths 配置
|
|
16
|
-
let configFilePath=null;if(fs.existsSync(tsConfigPath)){configFilePath=tsConfigPath}else if(fs.existsSync(jsConfigPath)){configFilePath=jsConfigPath}if(configFilePath){try{var _config$compilerOptio;const JSON5=require("json5");const configContent=fs.readFileSync(configFilePath,"utf-8");const config=JSON5.parse(configContent);const paths=((_config$compilerOptio=config.compilerOptions)===null||_config$compilerOptio===void 0?void 0:_config$compilerOptio.paths)||{};for(const[alias,targets]of Object.entries(paths)){var _targets$;// 将 tsconfig paths 格式 "foo/*" -> ["src/foo/*"] 转换为 vite alias
|
|
17
|
-
const cleanAlias=alias.replace("/*","");const cleanTarget=((_targets$=targets[0])===null||_targets$===void 0?void 0:_targets$.replace("/*",""))||"";if(cleanAlias&&cleanTarget){aliasConfig[cleanAlias]=path.resolve(cleanTarget)}}}catch(err){console.log("\x1B[33m\u26A0\uFE0F \u8BFB\u53D6\u8DEF\u5F84\u914D\u7F6E\u5931\u8D25: "+err.message+"\x1B[0m")}}// 默认别名
|
|
18
|
-
aliasConfig={assets:path.resolve("assets"),"@":path.resolve("src"),...aliasConfig};/** @type {import('vite').UserConfig} */const viteConfig={root:".",resolve:{alias:aliasConfig},plugins:[json5Plugin(),assetInlinePlugin(),fontFamilyPlugin(),socialMediaVitePlugin()],define:defines,server:{port:devOptions["port"]||3000,open:devOptions["open"]||false},build:{outDir:devOptions["outDir"]||"dist",sourcemap:true}};// 合并自定义配置
|
|
19
|
-
if(viteCustomConfig){const{plugins:customPlugins,define:customDefine,resolve:customResolve,server:customServer,build:customBuild,...rest}=viteCustomConfig;if(customPlugins){viteConfig.plugins=[...viteConfig.plugins,...customPlugins]}if(customDefine){viteConfig.define={...viteConfig.define,...customDefine}}if(customResolve){viteConfig.resolve={...viteConfig.resolve,...customResolve,alias:{...viteConfig.resolve.alias,...(customResolve.alias||{})}}}if(customServer){viteConfig.server={...viteConfig.server,...customServer}}if(customBuild){viteConfig.build={...viteConfig.build,...customBuild}}Object.assign(viteConfig,rest)}return viteConfig}/**
|
|
20
|
-
* 深度合并两个 Vite 配置对象。override 覆盖 base。
|
|
21
|
-
* 数组会拼接(plugins),其余字段浅覆盖。
|
|
22
|
-
*/function deepMergeViteConfig(base,override){if(!override||typeof override!=="object")return base;const result={...base};for(const key of Object.keys(override)){const bv=base[key];const ov=override[key];if(key==="plugins"&&Array.isArray(bv)&&Array.isArray(ov)){result[key]=[...bv,...ov]}else if(ov!==null&&typeof ov==="object"&&!Array.isArray(ov)&&bv!==null&&typeof bv==="object"&&!Array.isArray(bv)){result[key]={...bv,...ov}}else{result[key]=ov}}return result}/**
|
|
23
|
-
* 尝试加载项目根目录的 vite.config.ts。
|
|
24
|
-
*/async function loadProjectViteConfig(root){const candidates=["vite.config.ts","vite.config.js","vite.config.mjs"];let configPath=null;for(const c of candidates){const p=path.join(root,c);if(fs.existsSync(p)){configPath=p;break}}if(!configPath)return null;const{createRequire}=require("module");let projRequire;try{projRequire=createRequire(path.join(root,"package.json"))}catch(_){projRequire=createRequire(path.join(root,"noop.js"))}let loadConfigFromFile;try{loadConfigFromFile=projRequire("vite").loadConfigFromFile}catch(_){try{loadConfigFromFile=projRequire("vite/dist/node/config").loadConfigFromFile}catch(_2){console.log("\x1B[33m\u26A0\uFE0F [vite] Cannot load project config (loadConfigFromFile unavailable), using scripts default\x1B[0m");return null}}if(!loadConfigFromFile)return null;try{const result=await loadConfigFromFile({command:"serve",mode:process.env.NODE_ENV||"development"},configPath,root);if(result&&result.config){console.log(`[vite] Loaded project config: ${path.relative(root,configPath)}`);return result.config}return null}catch(err){console.log(`\x1b[33m⚠️ [vite] Failed to load ${path.relative(root,configPath)}: ${err.message}, using scripts default\x1b[0m`);return null}}/**
|
|
25
|
-
* 启动 Vite 开发服务器。
|
|
26
|
-
*
|
|
27
|
-
* 配置优先级(从低到高):
|
|
28
|
-
* 1. makeViteDevConfig 构建的默认配置(fontFamilyPlugin、json5Plugin 等)
|
|
29
|
-
* 2. 项目根目录的 vite.config.ts(如果存在,覆盖/扩充默认配置)
|
|
30
|
-
* 3. 传入的 viteConfig / customOptions / customDefines / viteCustomConfig
|
|
31
|
-
*/async function runViteDev(viteConfig,customOptions,customDefines,viteCustomConfig){if(!viteConfig){viteConfig=makeViteDevConfig(customOptions,customDefines,viteCustomConfig)}const projectRoot=process.cwd();const projectConfig=await loadProjectViteConfig(projectRoot);if(projectConfig){viteConfig=deepMergeViteConfig(viteConfig,projectConfig)}const{createRequire}=require("module");const projRequire=createRequire(path.join(projectRoot,"package.json"));const{createServer}=projRequire("vite");const server=await createServer({...viteConfig,configFile:false});await server.listen();server.printUrls()}// ── 导出 ──
|
|
32
|
-
exports.json5Plugin=json5Plugin;exports.assetInlinePlugin=assetInlinePlugin;exports.fontFamilyPlugin=fontFamilyPlugin;exports.makeViteDevConfig=makeViteDevConfig;exports.runViteDev=runViteDev;
|
|
1
|
+
"use strict";module.exports=require("./engines/web/vite.dev.js");
|
package/core/webpack.build.js
CHANGED
|
@@ -1,81 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
* HtmlWebpackPlugin beforeEmit 钩子。
|
|
3
|
-
* 在最终 HTML 输出前统一修正 meta 标签:
|
|
4
|
-
* - charset → 统一小写 utf-8
|
|
5
|
-
* - viewport → 统一替换为渠道通道规范值
|
|
6
|
-
* 项目模板无需关心 meta 标签,全部由 devkit 构建时自动处理。
|
|
7
|
-
*/class MetaTagOverridePlugin{constructor(adNetwork){this.adNetwork=adNetwork}apply(compiler){compiler.hooks.compilation.tap("MetaTagOverridePlugin",compilation=>{HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tap("MetaTagOverridePlugin",data=>{// charset: 统一小写
|
|
8
|
-
data.html=data.html.replace(/<meta\s+charset=["'][^"']*["']\s*\/?>/i,"<meta charset=\"utf-8\" />");// viewport: Mintegral 需要 minimum-scale,其他渠道通用
|
|
9
|
-
const viewportContent=this.adNetwork==="mintegral"?META.MINTEGRAL_VIEWPORT:META.COMMON_VIEWPORT;data.html=data.html.replace(/<meta\s+name=["']viewport["'][^>]*\/?>/i,`<meta name="viewport" content="${viewportContent}" />`);// font-family: Liftoff 渠道需要指定系统字体
|
|
10
|
-
if(this.adNetwork==="liftoff"){const fontStyle=FontFamily.LIFTOFF;data.html=data.html.replace("</head>",fontStyle+"</head>")}return data})})}}/** @type {AD_NETWORK[]} *//** @type {AD_NETWORK[]} */const zipOutputNetworks=["google","pangle","tiktok","vungle","mytarget","mintegral","adikteev","bigabid","inmobi",// InMobi 支持 ZIP 格式(包含 index.html + 资源文件)
|
|
11
|
-
"snapchat","bigoads","liftoff","facebook",// Facebook:ZIP 内仅 index.html(JS 全部内联),上限 5MB
|
|
12
|
-
"remerge"// Remerge Rich Media:ZIP + index.html + mraid.js
|
|
13
|
-
];/** @type {AD_NETWORK[]} */const zipOutputAllowedNetworks=["moloco"];/**
|
|
14
|
-
* Creates webpack configuration for production build
|
|
15
|
-
* @param {Partial<import('./index').CLIOptions>} [customOptions] - Custom options to merge with default options
|
|
16
|
-
* @param {Record<string, any>} [customDefines] - Additional defines for webpack.DefinePlugin
|
|
17
|
-
* @param {import('webpack').Configuration} [webpackCustomConfig] - Custom webpack config to merge
|
|
18
|
-
* @returns {import('webpack').Configuration} Final webpack production configuration
|
|
19
|
-
*/function makeWebpackBuildConfig(customOptions,customDefines,webpackCustomConfig){const buildOptions=mergeOptions(options,customOptions);logOptions(buildOptions);customDefines=customDefines||{};webpackCustomConfig=webpackCustomConfig||{};/** @type {AD_NETWORK} */const adNetwork=buildOptions["network"];/** @type {AD_PROTOCOL} */const adProtocol=buildOptions["protocol"];/** @type {'both'|'landscape'|'portrait'} 当前单次构建实际使用的横竖屏方向版本,默认 'both' */const playcraftOrientationValue=buildOptions.playcraftOrientationValue||"both";/** @type {string} 方向版本对应的命名后缀:landscape→'_L',portrait→'_P',both→'' */const orientationSuffix=getPlaycraftOrientationSuffix(playcraftOrientationValue);/** @type {string} 渠道 + 方向后缀组成的产物标识,如 'google_L' */const outputId=`${adNetwork}${orientationSuffix}`;let outDir=buildTemplateString(buildOptions.outDir,buildOptions);function getFileName(){let filename=buildTemplateString(buildOptions.filename,buildOptions);if(adNetwork==="mintegral")return filename.replace(/\s+/g,"").replace(/[^a-zA-Z0-9]/g,"_").replace("_fullhash_6_","[fullhash:6]");return filename}const isZipOutput=zipOutputNetworks.includes(adNetwork)||buildOptions.zip&&zipOutputAllowedNetworks.includes(adNetwork);const{isChannelFold2Zip,isZipOutputFinal}=resolveChannelFold2Zip({isChannelFold2ZipOption:buildOptions.isChannelFold2Zip,adNetwork,isZipOutput});let htmlFileName="";if(adNetwork==="mintegral")htmlFileName="index.html";// 修改为 index.html
|
|
20
|
-
else if(isZipOutputFinal)htmlFileName="index.html";else htmlFileName=`${getFileName()}.html`;const metaTags={viewport:META.COMMON_VIEWPORT};if(adNetwork==="mintegral"){metaTags["viewport"]=META.MINTEGRAL_VIEWPORT}if(!buildOptions.skipRecommendedMeta){metaTags["HandheldFriendly"]="True";metaTags["cleartype"]={"http-equiv":"cleartype",content:"on"};metaTags["apple-mobile-web-app-capable"]="yes";metaTags["mobile-web-app-capable"]="yes";metaTags["X-UA-Compatible"]={"http-equiv":"X-UA-Compatible",content:"IE=10"}}let htmlWebpackPluginConfig={template:path.resolve("src/index.html"),filename:htmlFileName,title:`${buildOptions.name} - ${buildOptions.app}`,inlineSource:".(js|css|png|jpg|webp|svg|xml|atlas|mp3|gif|glb|fbx|obj)$",meta:metaTags};if(adNetwork==="adikteev")htmlWebpackPluginConfig=generateAdikteevHtmlWebpackPluginConfig("src/index.html");else if(adNetwork==="bigabid")htmlWebpackPluginConfig=generateBigabidHtmlWebpackPluginConfig("src/index.html");else if(adNetwork==="inmobi")htmlWebpackPluginConfig=generateInMobiHtmlWebpackPluginConfig("src/index.html",buildOptions);const webpackConfig=merge(webpackCommonConfig,{mode:"production",stats:"errors-only",optimization:{usedExports:true,minimize:(buildOptions.obfuscateLevel!==undefined?buildOptions.obfuscateLevel:2)>=1,minimizer:[new TerserPlugin({extractComments:false,terserOptions:{safari10:true,mangle:{reserved:["$"],// ⚠️ 不要开启 properties: true,会混淆 Phaser 等框架的属性名导致运行时错误
|
|
21
|
-
// properties: true,
|
|
22
|
-
toplevel:true},compress:{drop_console:true,arrows:false,drop_debugger:true,unused:true,dead_code:true,// 🔥 增加优化轮数
|
|
23
|
-
passes:3,// 🔥 激进压缩选项
|
|
24
|
-
inline:3,// 最大内联
|
|
25
|
-
collapse_vars:true,// 折叠变量
|
|
26
|
-
reduce_vars:true,// 减少变量
|
|
27
|
-
hoist_vars:true,// 提升变量
|
|
28
|
-
hoist_funs:true,// 提升函数
|
|
29
|
-
sequences:20,// 更长的序列化
|
|
30
|
-
properties:true,// 优化属性访问
|
|
31
|
-
conditionals:true,// 优化条件
|
|
32
|
-
comparisons:true,// 优化比较
|
|
33
|
-
booleans:true,// 优化布尔
|
|
34
|
-
loops:true,// 优化循环
|
|
35
|
-
if_return:true,// 优化 if-return
|
|
36
|
-
join_vars:true,// 合并变量
|
|
37
|
-
negate_iife:true,// 否定 IIFE
|
|
38
|
-
side_effects:true,// 移除副作用
|
|
39
|
-
switches:true,// 优化 switch
|
|
40
|
-
typeofs:true,// 优化 typeof
|
|
41
|
-
unsafe:false,// 不使用 unsafe(避免破坏框架)
|
|
42
|
-
keep_fargs:false,// 移除未使用参数
|
|
43
|
-
drop_console:true},output:{comments:false,quote_style:3,beautify:false,wrap_iife:true,ecma:2015},toplevel:true,keep_classnames:false,keep_fnames:false}})]},plugins:[new HtmlWebpackPlugin(htmlWebpackPluginConfig),new MetaTagOverridePlugin(adNetwork),// 注入 window.playcraftOrientation 全局变量,标识当前构建产物对应的横竖屏方向版本
|
|
44
|
-
new PlaycraftOrientationInjectorPlugin(playcraftOrientationValue),new webpack.DefinePlugin({...buildDefines(buildOptions),...buildOptions.defines,...customDefines}),// For some reason needed for PIXI v8
|
|
45
|
-
new webpack.optimize.LimitChunkCountPlugin({maxChunks:1})]},webpackCustomConfig);webpackConfig.output.path=path.resolve(outDir);// 根据混淆等级注入 obfuscator loader
|
|
46
|
-
const obfuscateLevel=buildOptions.obfuscateLevel!==undefined?buildOptions.obfuscateLevel:2;// 获取需要排除压缩的目录列表
|
|
47
|
-
const excludeCompressDirs=buildOptions.excludeCompressDirs||[];if(obfuscateLevel>=2){// 构建各等级的 obfuscator 配置
|
|
48
|
-
const obfuscatorOptions={// 通用基础配置
|
|
49
|
-
target:"browser",sourceMap:false,selfDefending:false,renameProperties:false,disableConsoleOutput:true,splitStrings:false,unicodeEscapeSequence:false};if(obfuscateLevel===2){// 等级2:基础混淆(控制流平坦化 + 字符串数组)
|
|
50
|
-
Object.assign(obfuscatorOptions,{controlFlowFlattening:true,controlFlowFlatteningThreshold:0.75,deadCodeInjection:false,stringArray:true,stringArrayThreshold:0.75,stringArrayEncoding:["base64"],stringArrayIndexShift:true,stringArrayRotate:true,stringArrayShuffle:true,stringArrayWrappersCount:2,stringArrayWrappersChainedCalls:true,stringArrayWrappersType:"function",stringArrayCallsTransform:true,stringArrayCallsTransformThreshold:0.75,transformObjectKeys:true,identifierNamesGenerator:"hexadecimal"})}else if(obfuscateLevel===3){// 等级3:在等级2基础上增加死代码注入
|
|
51
|
-
Object.assign(obfuscatorOptions,{controlFlowFlattening:true,controlFlowFlatteningThreshold:0.75,deadCodeInjection:true,deadCodeInjectionThreshold:0.4,stringArray:true,stringArrayThreshold:0.75,stringArrayEncoding:["base64"],stringArrayIndexShift:true,stringArrayRotate:true,stringArrayShuffle:true,stringArrayWrappersCount:2,stringArrayWrappersChainedCalls:true,stringArrayWrappersType:"function",stringArrayCallsTransform:true,stringArrayCallsTransformThreshold:0.75,transformObjectKeys:true,identifierNamesGenerator:"hexadecimal"})}else if(obfuscateLevel===4){// 等级4:最高强度混淆
|
|
52
|
-
Object.assign(obfuscatorOptions,{controlFlowFlattening:true,controlFlowFlatteningThreshold:1,deadCodeInjection:true,deadCodeInjectionThreshold:0.5,stringArray:true,stringArrayThreshold:1,stringArrayEncoding:["rc4"],stringArrayIndexShift:true,stringArrayRotate:true,stringArrayShuffle:true,stringArrayWrappersCount:3,stringArrayWrappersChainedCalls:true,stringArrayWrappersType:"function",stringArrayCallsTransform:true,stringArrayCallsTransformThreshold:1,transformObjectKeys:true,identifierNamesGenerator:"hexadecimal",splitStrings:true,splitStringsChunkLength:5,unicodeEscapeSequence:true})}console.log(`Obfuscation level: ${obfuscateLevel} (${["","terser-only","basic","dead-code","maximum"][obfuscateLevel]})`);// 构建exclude数组,包含node_modules和用户指定的排除目录
|
|
53
|
-
const excludePatterns=[];if(excludeCompressDirs.length>0){excludeCompressDirs.forEach(dir=>{excludePatterns.push(path.resolve(dir))});console.log(`ℹ️ Excluding directories from compression: ${excludeCompressDirs.join(", ")}`)}webpackConfig.module.rules.push({test:/\.(js|ts)$/,include:[path.resolve("src")],exclude:excludePatterns,enforce:"post",use:{loader:require.resolve("webpack-obfuscator/dist/loader"),options:obfuscatorOptions}})}else if(obfuscateLevel===1){console.log("Obfuscation level: 1 (terser-only)")}else{console.log("Obfuscation level: 0 (no minimize/obfuscate)")}// Adikteev 需要单独提取 CSS 为 style.css,替换默认的 style-loader
|
|
54
|
-
if(adNetwork==="adikteev"){const cssRuleIndex=webpackConfig.module.rules.findIndex(rule=>rule.test&&rule.test.toString().includes("\\.css"));if(cssRuleIndex!==-1){webpackConfig.module.rules[cssRuleIndex]={test:/\.css$/,use:[MiniCssExtractPlugin.loader,"css-loader"]};console.log("\u2705 Adikteev: CSS will be extracted to style.css")}}// 决定是否内联 JS 到 HTML
|
|
55
|
-
// 以下渠道需要分离 JS 文件,不使用 HtmlInlineScriptPlugin:
|
|
56
|
-
// - mintegral: 需要独立的 JS 文件
|
|
57
|
-
// - adikteev: 需要 creative.js 上传到 CDN
|
|
58
|
-
// - bigabid: 需要 main.js 上传到 CDN
|
|
59
|
-
// Facebook ZIP 只要单文件 index.html,JS 必须内联
|
|
60
|
-
if(adNetwork!=="mintegral"&&adNetwork!=="adikteev"&&adNetwork!=="bigabid")webpackConfig.plugins.push(new HtmlInlineScriptPlugin);// 注入渠道 SDK 插件(公共逻辑)
|
|
61
|
-
injectSDKPlugins(webpackConfig,{network:adNetwork,protocol:adProtocol,playcraftOrientationValue});if(isZipOutputFinal){if(adNetwork==="tiktok"){webpackConfig.plugins.push(new CopyWebpackPlugin({patterns:[{from:path.join(__dirname,"resources","tiktok-config.json"),to:"config.json"}]}))}else if(adNetwork==="snapchat"){const tmpConfigPath=path.join(__dirname,"resources","snapchat-config.json");webpackConfig.plugins.push(new CopyWebpackPlugin({patterns:[{from:tmpConfigPath,to:"config.json"}]}))}else if(adNetwork==="bigoads"){// BigoAds 要求输出 ZIP 包,包含 index.html 和 config.json
|
|
62
|
-
// SDK 注入已在 injectSDKPlugins 中处理
|
|
63
|
-
webpackConfig.plugins.push(new CopyWebpackPlugin({patterns:[{from:path.join(__dirname,"resources","bigoads-config.json"),to:"config.json"}]}))}webpackConfig.plugins.push(new ZipPlugin({filename:isChannelFold2Zip?outputId:getFileName(),path:path.resolve(outDir),cleanTempFolder:true,// 🗑️ 生成 zip 后删除临时文件夹
|
|
64
|
-
// Facebook:zip 内只保留内联后的 index.html,不要带上 build.js
|
|
65
|
-
...(adNetwork==="facebook"?{include:/(?:^|\/)index\.html$/}:{})}));webpackConfig.output.path=path.resolve(outDir,outputId);if(adNetwork==="adikteev"){webpackConfig.output.filename="creative.js";// Adikteev 需要将 CSS 单独提取为 style.css 文件
|
|
66
|
-
webpackConfig.plugins.push(new MiniCssExtractPlugin({filename:"style.css"}))}else if(adNetwork==="bigabid")webpackConfig.output.filename="main.js";else if(adNetwork==="inmobi")webpackConfig.output.filename="main.js"}// 添加 fflate 压缩插件(仅在打正式包时生效)
|
|
67
|
-
if(buildOptions.fflateCompression){console.log("\u2705 fflate compression enabled (production build only)");webpackConfig.plugins.push(new FflateCompressionPlugin({enabled:true,level:9,keepOriginal:false,logStats:true,googlePlayUrl:buildOptions.googlePlayUrl,appStoreUrl:buildOptions.appStoreUrl}))}return webpackConfig}/**
|
|
68
|
-
* Runs webpack production build
|
|
69
|
-
* @param {import('webpack').Configuration} [webpackConfig] - Webpack configuration to use, creates default if not provided
|
|
70
|
-
* @param {Partial<import('./index').CLIOptions>} [customOptions] - Custom options to merge with default options
|
|
71
|
-
* @param {Record<string, any>} [customDefines] - Additional defines for webpack.DefinePlugin
|
|
72
|
-
* @param {import('webpack').Configuration} [webpackCustomConfig] - Custom webpack config to merge
|
|
73
|
-
*/function runBuild(webpackConfig,customOptions,customDefines,webpackCustomConfig){return new Promise((resolve,reject)=>{if(!webpackConfig)webpackConfig=makeWebpackBuildConfig(customOptions,customDefines,webpackCustomConfig);const compiler=webpack(webpackConfig);// 必须显式调用 compiler.close() 释放本次编译占用的内存(模块图/资源缓存/
|
|
74
|
-
// Terser 与 obfuscator 等插件持有的引用)。webpack 官方要求:若在同一进程内
|
|
75
|
-
// 多次调用 compiler.run(),每次 run 完成后都要 close,否则上一次编译的内存
|
|
76
|
-
// 无法被回收。playcraftOrientationChannel 会在同一进程里串行构建多个方向
|
|
77
|
-
// 版本(多次调用本函数),若不 close,内存会随构建次数线性累加,最终触发
|
|
78
|
-
// "JavaScript heap out of memory"。
|
|
79
|
-
const finalize=error=>{compiler.close(closeErr=>{if(closeErr){console.error("\u5173\u95ED webpack compiler \u65F6\u51FA\u9519:",closeErr)}if(error){reject(error)}else{resolve()}})};compiler.run((err,stats)=>{if(err){console.error("Build failed:",err.stack||err);if(err.details){console.error("Error details:",err.details)}return finalize(err)}if(stats.hasErrors()){const errors=stats.compilation.errors;console.error(`\n❌ Build finished with ${errors.length} error(s):\n`);// 打印每个错误的详细信息
|
|
80
|
-
errors.forEach((error,index)=>{console.error(`\n[Error ${index+1}/${errors.length}]`);console.error(error.message||error);if(error.stack){console.error(error.stack)}});// 拒绝 Promise 时传递格式化的错误消息
|
|
81
|
-
const errorMessage=errors.map((e,i)=>`[${i+1}] ${e.message||e}`).join("\n");finalize(new Error(`Webpack build failed with ${errors.length} error(s):\n${errorMessage}`))}else{console.log(`Build successful!`);finalize(null)}})})}exports.makeWebpackBuildConfig=makeWebpackBuildConfig;exports.runBuild=runBuild;
|
|
1
|
+
"use strict";module.exports=require("./engines/web/webpack.build.js");
|
package/core/webpack.common.js
CHANGED
|
@@ -1,11 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
{test:/\.vue$/,loader:"vue-loader"},// {
|
|
3
|
-
// test: /\.tsx?$/,
|
|
4
|
-
// use: 'ts-loader',
|
|
5
|
-
// exclude: /node_modules/,
|
|
6
|
-
// },
|
|
7
|
-
{test:/\.ts?$/,exclude:/node_modules/,use:[{loader:"babel-loader",options:{// presets: ['@babel/preset-env'],
|
|
8
|
-
plugins:options.compilation.allowTemplateLiterals===false?["@babel/plugin-transform-template-literals"]:[]}},{loader:"esbuild-loader",options:{loader:"ts",target:"es2015"}}]},{test:/\.css$/,use:["style-loader","css-loader"]},{test:/\.(js|mjs)$/,// exclude: /node_modules/,
|
|
9
|
-
use:{loader:"babel-loader",options:{// presets: ['@babel/preset-env'],
|
|
10
|
-
plugins:options.compilation.allowTemplateLiterals===false?["@babel/plugin-transform-template-literals"]:[]}}},{test:/\.(gltf)$/,loader:path.join(__dirname,"loaders/gltf-loader.js")},{test:/\.(gif|png|jpe?g|webp|mp3|mp4|m4a|ogg|wav|glb|fbx|obj)$/i,type:"asset/inline"},{test:/\.json5$/i,type:"javascript/auto",use:{loader:"json5-loader",options:{esModule:false}}},// 处理 json?url → base64
|
|
11
|
-
{test:/\.json$/i,resourceQuery:/url/,type:"asset/inline",generator:{dataUrl:content=>`data:application/json;base64,${content.toString("base64")}`}},{test:/\.(svg|xml)$/i,type:"asset/source"},{test:/\.atlas$/,type:"asset/inline",generator:{dataUrl:content=>{return`data:text/atlas;base64,${content.toString("base64")}`}}},{test:/\.fnt$/i,type:"asset/inline",generator:{dataUrl:content=>{return`data:text/plain;base64,${content.toString("base64")}`}}},{test:/\.(woff|woff2|eot|ttf|otf)$/i,type:"asset/inline"}]},plugins:[new VueLoaderPlugin]};exports.allowedExtensions=allowedExtensions;exports.webpackCommonConfig=webpackConfig;
|
|
1
|
+
"use strict";module.exports=require("./engines/web/webpack.common.js");
|
package/core/webpack.dev.js
CHANGED
|
@@ -1,16 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
* Creates webpack configuration for development
|
|
3
|
-
* @param {Partial<import('./index').CLIOptions>} [customOptions] - Custom options to merge with default options
|
|
4
|
-
* @param {Record<string, any>} [customDefines] - Additional defines for webpack.DefinePlugin
|
|
5
|
-
* @param {import('webpack').Configuration} [webpackCustomConfig] - Custom webpack config to merge
|
|
6
|
-
* @returns {import('webpack').Configuration} Final webpack development configuration
|
|
7
|
-
*/function makeWebpackDevConfig(customOptions,customDefines,webpackCustomConfig){const devOptions=mergeOptions(options,customOptions);logOptions(devOptions);customDefines=customDefines||{};webpackCustomConfig=webpackCustomConfig||{};/** @type {'both'|'landscape'|'portrait'} */const playcraftOrientationValue=devOptions.playcraftOrientationValue||"both";const webpackConfig=merge(webpackCommonConfig,{mode:"development",devtool:"inline-source-map",devServer:{static:{directory:path.resolve("dist")},hot:true,// compress: true,
|
|
8
|
-
port:devOptions["port"],allowedHosts:"all",open:devOptions["open"]},plugins:[new HtmlWebpackPlugin({template:path.resolve("src/index.html"),inlineSource:".(js|css|png|jpg|webp|svg|xml|atlas|mp3|gif|glb|fbx|obj)$",meta:{viewport:"width=device-width,initial-scale=1.0,viewport-fit=cover,maximum-scale=1.0,user-scalable=no"}}),// 注入 window.playcraftOrientation 全局变量,标识当前构建产物对应的横竖屏方向版本
|
|
9
|
-
new PlaycraftOrientationInjectorPlugin(playcraftOrientationValue),new webpack.DefinePlugin({...buildDefines(devOptions),...devOptions.defines,__DEV__:JSON.stringify(devOptions["dev"]===undefined?true:devOptions["dev"]),...customDefines})]},webpackCustomConfig);if(devOptions["debugger"]){webpackConfig.plugins.push(new DebuggerInjectionPlugin(devOptions["debugger"]))}// 注入渠道 SDK 插件(与 build 保持一致)
|
|
10
|
-
injectSDKPlugins(webpackConfig,{network:devOptions["network"],protocol:devOptions["protocol"],playcraftOrientationValue});return webpackConfig}/**
|
|
11
|
-
* Starts webpack development server
|
|
12
|
-
* @param {import('webpack').Configuration} [webpackConfig] - Webpack configuration to use, creates default if not provided
|
|
13
|
-
* @param {Partial<import('./index').CLIOptions>} [customOptions] - Custom options to merge with default options
|
|
14
|
-
* @param {Record<string, any>} [customDefines] - Additional defines for webpack.DefinePlugin
|
|
15
|
-
* @param {import('webpack').Configuration} [webpackCustomConfig] - Custom webpack config to merge
|
|
16
|
-
*/function runDev(webpackConfig,customOptions,customDefines,webpackCustomConfig){if(!webpackConfig)webpackConfig=makeWebpackDevConfig(customOptions,customDefines,webpackCustomConfig);const compiler=webpack(webpackConfig);const server=new WebpackDevServer(webpackConfig.devServer,compiler);server.start()}exports.makeWebpackDevConfig=makeWebpackDevConfig;exports.runDev=runDev;
|
|
1
|
+
"use strict";module.exports=require("./engines/web/webpack.dev.js");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/devkit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.31-beta.10",
|
|
4
4
|
"description": "HTML5 Playable Ads 构建工具,支持多广告渠道打包",
|
|
5
5
|
"main": "core/index.js",
|
|
6
6
|
"module": "core/index.js",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@vue/compiler-sfc": "^3.4.0",
|
|
28
28
|
"ajv": "^8.18.0",
|
|
29
29
|
"babel-loader": "^9.2.1",
|
|
30
|
+
"cheerio": "^1.0.0-rc.12",
|
|
30
31
|
"copy-webpack-plugin": "^13.0.0",
|
|
31
32
|
"cos-nodejs-sdk-v5": "^2.14.4",
|
|
32
33
|
"cross-spawn": "^7.0.3",
|
|
@@ -37,7 +38,9 @@
|
|
|
37
38
|
"html-webpack-plugin": "^5.6.0",
|
|
38
39
|
"json5": "^2.2.3",
|
|
39
40
|
"json5-loader": "^4.0.0",
|
|
41
|
+
"mime-types": "^2.1.35",
|
|
40
42
|
"mini-css-extract-plugin": "^2.7.6",
|
|
43
|
+
"pako": "^2.2.0",
|
|
41
44
|
"prettyjson": "^1.2.5",
|
|
42
45
|
"style-loader": "^4.0.0",
|
|
43
46
|
"tencentcloud-sdk-nodejs-cdn": "^4.1.161",
|