@playcraft/devkit 1.0.31-beta.12 → 1.0.31-beta.14

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.
@@ -0,0 +1 @@
1
+ "use strict";const{describe,it}=require("node:test");const assert=require("node:assert/strict");function stripUrl(url){url=String(url||"").trim().split("#")[0].split("?")[0];if(!url)return"";try{if(/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)){const parsed=new URL(url,"https://playable.local/");if(parsed.protocol==="blob:"||parsed.protocol==="data:")return"";url=decodeURIComponent(parsed.pathname||"")}}catch(err){}url=url.replace(/^\/+/,"");const parts=[];url.split("/").forEach(part=>{if(!part||part===".")return;if(part===".."){if(parts.length)parts.pop();return}parts.push(part)});return parts.join("/")}function bestKey(url,target){if(!url||!target)return null;const raw=String(url).split("#")[0].split("?")[0];const stripped=stripUrl(url);if(target[raw])return raw;if(stripped&&target[stripped])return stripped;let best=null;function consider(candidate){if(!candidate||candidate.length<3)return;for(const key in target){if(candidate===key||candidate.endsWith("/"+key)){if(!best||key.length>best.length)best=key}}}consider(stripped);consider(raw);return best}describe("adapter VFS path lookup",()=>{const js={"cocos-js/cc.js":"engine","assets/main/index.js":"game","src/import-map.json":"{\"imports\":{\"cc\":\"./../cocos-js/cc.js\"}}"};it("maps Facebook and Liftoff CDN URLs to the VFS key without changing System ids",()=>{assert.equal(bestKey("https://www.facebook.com/developer/tools/cocos-js/cc.js",js),"cocos-js/cc.js");assert.equal(bestKey("https://cdn.liftoff-creatives.io/tmp/32db3a48-516d-41fb-8508-d1db75caca7e/cocos-js/cc.js",js),"cocos-js/cc.js")});it("maps import-map relative targets after collapsing ..",()=>{assert.equal(bestKey("./../cocos-js/cc.js",js),"cocos-js/cc.js")});it("does not map a short basename onto a different index.js",()=>{assert.equal(bestKey("./dist/index.js",js),null);assert.equal(bestKey("https://example.com/foo/dist/index.js",js),null)});it("resolves the import-map specifier cc to its file target",()=>{const imports=JSON.parse(js["src/import-map.json"]).imports;assert.equal(imports.cc,"./../cocos-js/cc.js");assert.equal(bestKey(imports.cc,js),"cocos-js/cc.js")})});
@@ -3,9 +3,9 @@ function base64ToBlob(base64,type){let oriResBase64=base64;let base64Arr=oriResB
3
3
  if(base64Arr.length===2){let mimeStr=base64Arr.shift();let regExp=new RegExp(":(.*?);");let array=mimeStr.match(regExp);mime=(array&&array.length>1?array[1]:type)||type}// mime-type end
4
4
  // arraybuffer start
5
5
  let base64Str=base64Arr.shift();let bytes=window.atob(base64Str);let arrBuf=new ArrayBuffer(bytes.length);// arraybuffer end
6
- const u8arr=new Uint8Array(arrBuf);for(let i=0;i<bytes.length;i++){u8arr[i]=bytes.charCodeAt(i)}return new Blob([arrBuf],{type:mime})}function base64toArrayBuffer(base64){var bstr=window.atob(base64.substring(base64.indexOf(",")+1));let index=bstr.length;let u8arr=new Uint8Array(index);while(index--){u8arr[index]=bstr.charCodeAt(index)}return u8arr.buffer}class AdapterFetch{constructor(){this.response=null,this.status=200,this.responseType="",this.onload=function(){console.log("onload")}}open(e,t){const n=__adapter_get_resource(t);n?this.response=n:console.log("res",n,t)}send(){switch(this.responseType){case"json":this.response=JSON.parse(this.response);break;case"text":this.response=this.response;break;case"arraybuffer":this.response=base64toArrayBuffer(this.response);break;default:this.response=this.response}this.onload()}}function __adapter_eval(name){if(!window.__adapter_js__[name]){console.warn("window.__adapter_js__ is not found ",name);return}eval(window.__adapter_js__[name]);delete window.__adapter_js__[name]}function __adapter_get_base_url(){const hasDocument=typeof document!=="undefined";let baseUrl;if(hasDocument){let baseEl=document.querySelector("base[href]");if(baseEl)baseUrl=baseEl.href}if(!baseUrl&&typeof location!=="undefined"){baseUrl=location.href.split("#")[0].split("?")[0];var lastSepIndex=baseUrl.lastIndexOf("/");if(lastSepIndex!==-1)baseUrl=baseUrl.slice(0,lastSepIndex+1)}return baseUrl||""}function __adapter_get_res_path(url,target){if(target[url]){return url}for(let key in target){const index=url.indexOf(key);if(index!==-1&&index+key.length===url.length){return key}}return null}function __adapter_get_resource(url){return window.__adapter_resource__[__adapter_get_res_path(url,window.__adapter_resource__)]}function __adapter_get_imports(){const json=JSON.parse(__adapter_get_resource("src/import-map.json"));return json.imports}function __adapter_get_script(url){if(url.indexOf("bullet.wasm")!==-1){for(let k2 in window.__adapter_js__){if(k2.indexOf("bullet.cocos")!==-1){url=k2;break}}}let _key=__adapter_get_res_path(url,window.__adapter_js__);const res=window.__adapter_js__[_key];if(res){delete window.__adapter_js__[_key]}return res}function __adapter_init_http(){window.adapterFetch=AdapterFetch;const _fetch=window.fetch;window.fetch=function(url,options){const resource=__adapter_get_resource(url);if(resource){// 模拟fetch响应结果
6
+ const u8arr=new Uint8Array(arrBuf);for(let i=0;i<bytes.length;i++){u8arr[i]=bytes.charCodeAt(i)}return new Blob([arrBuf],{type:mime})}function base64toArrayBuffer(base64){var bstr=window.atob(base64.substring(base64.indexOf(",")+1));let index=bstr.length;let u8arr=new Uint8Array(index);while(index--){u8arr[index]=bstr.charCodeAt(index)}return u8arr.buffer}class AdapterFetch{constructor(){this.response=null,this.status=200,this.responseType="",this.onload=function(){console.log("onload")}}open(e,t){const n=__adapter_get_resource(t);n?this.response=n:console.log("res",n,t)}send(){switch(this.responseType){case"json":this.response=JSON.parse(this.response);break;case"text":this.response=this.response;break;case"arraybuffer":this.response=base64toArrayBuffer(this.response);break;default:this.response=this.response}this.onload()}}function __adapter_eval(name){if(!window.__adapter_js__[name]){console.warn("window.__adapter_js__ is not found ",name);return}eval(window.__adapter_js__[name]);delete window.__adapter_js__[name]}function __adapter_get_base_url(){const hasDocument=typeof document!=="undefined";let baseUrl;if(hasDocument){let baseEl=document.querySelector("base[href]");if(baseEl)baseUrl=baseEl.href}if(!baseUrl&&typeof location!=="undefined"){baseUrl=location.href.split("#")[0].split("?")[0];var lastSepIndex=baseUrl.lastIndexOf("/");if(lastSepIndex!==-1)baseUrl=baseUrl.slice(0,lastSepIndex+1)}return baseUrl||""}function __adapter_strip_url(url){url=String(url||"").trim().split("#")[0].split("?")[0];if(!url)return"";try{if(/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)){const parsed=new URL(url,"https://playable.local/");if(parsed.protocol==="blob:"||parsed.protocol==="data:")return"";url=decodeURIComponent(parsed.pathname||"")}}catch(err){}url=url.replace(/^\/+/,"");const parts=[];url.split("/").forEach(function(part){if(!part||part===".")return;if(part===".."){if(parts.length)parts.pop();return}parts.push(part)});return parts.join("/")}function __adapter_best_key(url,target){if(!url||!target)return null;const raw=String(url).split("#")[0].split("?")[0];const stripped=__adapter_strip_url(url);if(target[raw])return raw;if(stripped&&target[stripped])return stripped;let best=null;function consider(candidate){if(!candidate||candidate.length<3)return;for(const key in target){if(candidate===key||candidate.endsWith("/"+key)){if(!best||key.length>best.length)best=key}}}consider(stripped);consider(raw);if(raw&&target[raw.replace(__adapter_get_base_url(),"")]){return raw.replace(__adapter_get_base_url(),"")}return best}function __adapter_get_res_path(url,target){return __adapter_best_key(url,target)}function __adapter_get_resource(url){const key=__adapter_best_key(url,window.__adapter_resource__);return key?window.__adapter_resource__[key]:undefined}function __adapter_get_imports(){const json=JSON.parse(__adapter_get_resource("src/import-map.json"));return json.imports}function __adapter_import_target(id){try{const imports=__adapter_get_imports()||{};if(imports[id])return imports[id]}catch(err){}return null}function __adapter_read_js(key){if(!key)return null;if(window.__adapter_js__&&window.__adapter_js__[key]!=null){return window.__adapter_js__[key]}if(window.__adapter_resource__&&window.__adapter_resource__[key]!=null){return window.__adapter_resource__[key]}return null}function __adapter_get_script(url){if(String(url).indexOf("bullet.wasm")!==-1){const js=window.__adapter_js__||{};for(let k2 in js){if(k2.indexOf("bullet.cocos")!==-1){url=k2;break}}}const tables=[window.__adapter_js__,window.__adapter_resource__];const candidates=[url,__adapter_import_target(url)];for(let c=0;c<candidates.length;c++){if(!candidates[c])continue;for(let i=0;i<tables.length;i++){const key=__adapter_best_key(candidates[c],tables[i]);const res=__adapter_read_js(key);if(res!=null)return res}}return null}function __adapter_import_map_present(){if(typeof document==="undefined")return false;const el=document.querySelector("script[type=\"systemjs-importmap\"]");if(!el||!el.textContent)return false;try{const map=JSON.parse(el.textContent);return!!(map&&map.imports&&Object.keys(map.imports).length)}catch(err){return false}}function __adapter_ensure_import_map(){if(typeof document==="undefined")return;if(__adapter_import_map_present())return;const raw=__adapter_get_resource("src/import-map.json");if(!raw)return;const el=document.createElement("script");el.type="systemjs-importmap";el.textContent=typeof raw==="string"?raw:JSON.stringify(raw);(document.head||document.body||document.documentElement).appendChild(el)}function __adapter_init_http(){window.adapterFetch=AdapterFetch;const _fetch=window.fetch;window.fetch=function(url,options){const resource=__adapter_get_resource(url);if(resource){// 模拟fetch响应结果
7
7
  let response={ok:true,status:200,statusText:"OK",headers:{get:function(key){// 根据不同的资源url返回不同的content-type,json、css、wasm
8
- if(key==="content-type"){if(url.indexOf(".json")!==-1){return"application/json"}else if(url.indexOf(".css")!==-1){return"text/css"}else if(url.indexOf(".wasm")!==-1){return"application/wasm"}}return"application/javascript"}},url:url,clone:function(){return response},text:function(){return Promise.resolve(resource)},json:function(){return Promise.resolve(JSON.parse(resource))},arrayBuffer:function(){return Promise.resolve(base64toArrayBuffer(resource))}};return Promise.resolve(response)}return _fetch(url,options)}}function __adapter_init_js(){const _createScript=System.__proto__.createScript;System.__proto__.createScript=function(url){let baseUrl=url.replace(__adapter_get_base_url(),"");let res=__adapter_get_script(baseUrl);if(!res){console.error(`${url} 找不到资源`);return _createScript.call(this,url)}const blob=new Blob([res],{type:"text/javascript"});return _createScript.call(this,URL.createObjectURL(blob))}}function __adapter_init_cc(){if(window.__adapter_cc_initialized__){return}window.__adapter_cc_initialized__=true;function loadScript(url,options,onComplete){let scriptStr=__adapter_get_resource(url);if(!scriptStr){console.error(url+" isn't load")}eval(scriptStr);onComplete&&onComplete(null)}// Json
8
+ if(key==="content-type"){if(url.indexOf(".json")!==-1){return"application/json"}else if(url.indexOf(".css")!==-1){return"text/css"}else if(url.indexOf(".wasm")!==-1){return"application/wasm"}}return"application/javascript"}},url:url,clone:function(){return response},text:function(){return Promise.resolve(resource)},json:function(){return Promise.resolve(JSON.parse(resource))},arrayBuffer:function(){return Promise.resolve(base64toArrayBuffer(resource))}};return Promise.resolve(response)}return _fetch(url,options)}}function __adapter_init_resolve(){if(!window.System||typeof System.resolve!=="function")return;const origResolve=System.resolve.bind(System);System.resolve=function(id,parentURL){const mapped=__adapter_import_target(id);try{const resolved=origResolve(id,parentURL);if(mapped&&(resolved===id||!/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(String(resolved)))){return origResolve(mapped)}return resolved}catch(err){if(mapped)return origResolve(mapped);throw err}}}function __adapter_init_js(){const _createScript=System.__proto__.createScript;System.__proto__.createScript=function(url){const res=__adapter_get_script(url)||__adapter_get_script(String(url).replace(__adapter_get_base_url(),""));if(!res){const mapped=__adapter_import_target(url);if(mapped){const mappedRes=__adapter_get_script(mapped);if(mappedRes){return _createScript.call(this,URL.createObjectURL(new Blob([mappedRes],{type:"text/javascript"})))}}console.error(`${url} 找不到资源`);return _createScript.call(this,url)}const blob=new Blob([res],{type:"text/javascript"});return _createScript.call(this,URL.createObjectURL(blob))}}function __adapter_init_cc(){if(window.__adapter_cc_initialized__){return}window.__adapter_cc_initialized__=true;function loadScript(url,options,onComplete){let scriptStr=__adapter_get_resource(url);if(!scriptStr){console.error(url+" isn't load")}eval(scriptStr);onComplete&&onComplete(null)}// Json
9
9
  function loadJson(url,options,onComplete){const data=JSON.parse(__adapter_get_resource(url));onComplete(null,data)}function loadBundle(url,options,onComplete){const REGEX=new RegExp("^(?:w+://|.+/).+");let bundleName=cc.path.basename(url);if(!REGEX.test(url)){url="assets/"+bundleName}const version=options.version||cc.assetManager.downloader.bundleVers[bundleName];let count=0;const config=`${url}/config.${version?version+".":""}json`;let out=null,error=null;loadJson(config,options,function(err,response){if(err){error=err}out=response;out&&(out.base=url+"/");count++;if(count===2){onComplete&&onComplete(error,out)}});const js=`${url}/index.${version?version+".":""}js`;loadScript(js,options,function(err){if(err){error=err}count++;if(count===2){onComplete&&onComplete(error,out)}})}function loadFont(url,options,onComplete){const fontFamilyName=url.split("/").pop().split(".")[0];const data=__adapter_get_resource(url);if(data==null){onComplete();return}const fontFace=new FontFace(fontFamilyName,"url("+data+")");document.fonts.add(fontFace);fontFace.load();fontFace.loaded.then(function(){onComplete(null,fontFamilyName)},function(){console.error("url("+url+") load fail");onComplete(null,fontFamilyName)})}// Image
10
10
  function loadImage(url,options,onComplete){const data=__adapter_get_resource(url);if(!data){console.error(url+" isn't load")}let img=new Image;const loadCallback=function(){img.removeEventListener("load",loadCallback);img.removeEventListener("error",errorCallback);onComplete&&onComplete(null,img)};const errorCallback=function(){img.removeEventListener("load",loadCallback);img.removeEventListener("error",errorCallback);onComplete&&onComplete(new Error(cc.debug.getError(4930,url)))};img.addEventListener("load",loadCallback);img.addEventListener("error",errorCallback);img.src=data;return img}// Video
11
11
  function loadVideo(url,options,onComplete){var video=document.createElement("video");var source=document.createElement("source");video.appendChild(source);let res=__adapter_get_resource(url);if(res){res=base64ToBlob(res);res=URL.createObjectURL(res);source.src=res;onComplete(null,video)}else{onComplete(new Error(url+"(no response)"))}}// Text
@@ -16,4 +16,4 @@ function loadArrayBuffer(url,options,onComplete){let data=__adapter_get_resource
16
16
  ".png":loadImage,".jpg":loadImage,".bmp":loadImage,".jpeg":loadImage,".gif":loadImage,".ico":loadImage,".tiff":loadImage,".webp":loadImage,".image":loadImage,// Videos
17
17
  ".mp4":loadVideo,".avi":loadVideo,".mov":loadVideo,".mpg":loadVideo,".mpeg":loadVideo,".rm":loadVideo,".rmvb":loadVideo,// Text
18
18
  ".txt":loadText,".xml":loadText,".vsh":loadText,".fsh":loadText,".atlas":loadText,".tmx":loadText,".tsx":loadText,".plist":loadText,".fnt":loadText,// ArrayBuffer
19
- ".pvr":loadArrayBuffer,".pkm":loadArrayBuffer,".astc":loadArrayBuffer,".binary":loadArrayBuffer,".bin":loadArrayBuffer,".dbbin":loadArrayBuffer,".skel":loadArrayBuffer,"bundle":loadBundle,"default":loadText};cc.assetManager.downloader.downloadScript=loadScript;Object.keys(downloaderList).forEach(extname=>{cc.assetManager.downloader.register(extname,downloaderList[extname])})}function __adapter_init_plugins(){if(!window.__adapter_plugins__||window.__adapter_plugins__.length===0){return}window.__adapter_plugins__.forEach(scriptPath=>{const fileName="src/"+scriptPath;__adapter_eval(fileName)})}function __adapter_get_path(key){for(var k in window.__adapter_resource__){if(k.indexOf(key)==0){return k}}throw Error("no find "+key)}__adapter_init_http();__adapter_eval(__adapter_get_path("src/polyfills.bundle"));__adapter_eval(__adapter_get_path("src/system.bundle"));__adapter_init_js();let prepareLoad=Promise.resolve();const importsKeys=Object.keys(__adapter_get_imports());for(let index=importsKeys.length-1;index>=0;index--){const key=importsKeys[index];prepareLoad=prepareLoad.then(()=>System.import(key))}prepareLoad.then(()=>{__adapter_init_cc();__adapter_init_plugins();System.import("./"+__adapter_get_path("index")).catch(err=>{console.error(err)})})};
19
+ ".pvr":loadArrayBuffer,".pkm":loadArrayBuffer,".astc":loadArrayBuffer,".binary":loadArrayBuffer,".bin":loadArrayBuffer,".dbbin":loadArrayBuffer,".skel":loadArrayBuffer,"bundle":loadBundle,"default":loadText};cc.assetManager.downloader.downloadScript=loadScript;Object.keys(downloaderList).forEach(extname=>{cc.assetManager.downloader.register(extname,downloaderList[extname])})}function __adapter_init_plugins(){if(!window.__adapter_plugins__||window.__adapter_plugins__.length===0){return}window.__adapter_plugins__.forEach(scriptPath=>{const fileName="src/"+scriptPath;__adapter_eval(fileName)})}function __adapter_get_path(key){for(var k in window.__adapter_resource__){if(k.indexOf(key)==0){return k}}throw Error("no find "+key)}__adapter_init_http();__adapter_ensure_import_map();__adapter_eval(__adapter_get_path("src/polyfills.bundle"));__adapter_eval(__adapter_get_path("src/system.bundle"));__adapter_init_resolve();__adapter_init_js();let prepareLoad=Promise.resolve();const importsKeys=Object.keys(__adapter_get_imports());for(let index=importsKeys.length-1;index>=0;index--){const key=importsKeys[index];prepareLoad=prepareLoad.then(()=>System.import(key))}prepareLoad.then(()=>{__adapter_init_cc();__adapter_init_plugins();System.import("./"+__adapter_get_path("index")).catch(err=>{console.error(err)})})};
@@ -0,0 +1 @@
1
+ "use strict";const{describe,it,afterEach}=require("node:test");const assert=require("node:assert/strict");const fs=require("fs");const os=require("os");const path=require("path");const vm=require("vm");const{load}=require("cheerio");const fflate=require("fflate");const{mergeWebMobile,escapeInlineScriptJson}=require("./merge.js");function writeFile(file,body){fs.mkdirSync(path.dirname(file),{recursive:true});fs.writeFileSync(file,body)}describe("escapeInlineScriptJson",()=>{it("escapes script terminators inside json strings",()=>{const escaped=escapeInlineScriptJson(JSON.stringify({code:"if (a < b) </script>"}));assert.equal(escaped.includes("</script>"),false);assert.equal(escaped.includes("<"),false);assert.equal(JSON.parse(escaped).code,"if (a < b) </script>")})});describe("mergeWebMobile resource map",()=>{let root;afterEach(()=>{if(root&&fs.existsSync(root))fs.rmSync(root,{recursive:true,force:true})});it("inlines the resource map without pako and keeps script text executable",async()=>{root=fs.mkdtempSync(path.join(os.tmpdir(),"cocos-merge-inline-"));const web=path.join(root,"web-mobile");writeFile(path.join(web,"index.html"),"<!DOCTYPE html><html><head></head><body><div id=\"GameDiv\"></div></body></html>");writeFile(path.join(web,"src","settings.json"),JSON.stringify({plugins:{jsList:[]},splashScreen:{totalTime:500,logo:{type:"default",base64:"x"}}}));writeFile(path.join(web,"assets","main","index.js"),"function demo(a,b){return a < b && \"</script>\";}");const out=path.join(root,"single.html");await mergeWebMobile(web,out,{isZip:false,obfuscateLevel:0});const html=fs.readFileSync(out,"utf8");assert.equal(html.includes("data-id=\"jszip\""),false);assert.equal(html.includes("data-id=\"adapter-zip-0\""),false);const readResource=page=>{const code=load(page)("script[data-id=\"adapter-resource-0\"]").text();const sandbox={window:{}};vm.runInNewContext(code,sandbox);return sandbox.window.__adapter_resource__};const resource=readResource(html);assert.equal(resource["assets/main/index.js"],"function demo(a,b){return a < b && \"</script>\";}");const compressed=fflate.zlibSync(Buffer.from(html));const restored=Buffer.from(fflate.unzlibSync(compressed)).toString("utf8");assert.equal(readResource(restored)["assets/main/index.js"],"function demo(a,b){return a < b && \"</script>\";}");assert.ok(compressed.length<Buffer.byteLength(html))})});
@@ -1,6 +1,12 @@
1
- "use strict";const fs=require("fs");const path=require("path");const{load}=require("cheerio");const{deflate}=require("pako");const{getResourceMapper,getBase64FromFile}=require("./fs");const{TRANSPARENT_GIF}=require("./constants");const{applyObfuscation,isCocosBusinessScript,obfuscateLevelName}=require("../../utils/obfuscateLevels.js");function readInject(rel){return fs.readFileSync(path.join(__dirname,rel),"utf8")}function paddingStyleTags($,originPkgPath,enableSplash){$("link[type=\"text/css\"]").toArray().forEach(item=>{const href=$(item).attr("href");if(!href)return;const cssPath=path.join(originPkgPath,href);if(!fs.existsSync(cssPath))return;const cssStr=fs.readFileSync(cssPath,"utf-8");$(`<style>${cssStr}</style>`).appendTo("head")});if(!enableSplash){$("<style>#splash,#splash .progress-bar{display:none!important;opacity:0;visibility:hidden}</style>").appendTo("head")}$("link[type=\"text/css\"]").remove();$("head").find("style").each((_index,elem)=>{const cssUrlReg=/url\("?'?.*"?'?\)/g;let styleTagStr=$(elem).html()||"";const matchStrList=styleTagStr.match(cssUrlReg);if(!matchStrList)return;matchStrList.forEach(str=>{const imgUrl=str.replace(/"|'|url|\(|\)/g,"");const imgPath=path.join(originPkgPath,imgUrl);const imgBase64=enableSplash&&fs.existsSync(imgPath)?getBase64FromFile(imgPath):TRANSPARENT_GIF;styleTagStr=styleTagStr.replace(str,`url(${imgBase64})`)});$(elem).html(styleTagStr)})}function paddingScriptTags($,originPkgPath){let scriptTags="";$("script[type=\"systemjs-importmap\"]").toArray().forEach(item=>{const href=$(item).attr("src");if(!href)return;const srcPath=path.join(originPkgPath,href);if(!fs.existsSync(srcPath))return;scriptTags+=`<script type="systemjs-importmap">${fs.readFileSync(srcPath,"utf-8")}</script>`});$("head link").remove();$("body script").remove();if(scriptTags)$(scriptTags).appendTo("body")}function getJsListFromSettingsJson(data){const jsonData=JSON.parse(data);jsonData.plugins={jsList:[],...jsonData.plugins};const jsList=[...jsonData.plugins.jsList];jsonData.plugins.jsList=[];return{jsList,settingsData:jsonData}}function stripSplashScreen(settingsData){if(!settingsData||!settingsData.splashScreen)return settingsData;settingsData.splashScreen.totalTime=0;settingsData.splashScreen.logo={type:"none",base64:""};return settingsData}function getJsListFromSettingsJs(data){const originData={jsList:[],settingsData:data};const settingsStrList=data.split("jsList:");if(settingsStrList.length<2)return originData;const settingsStr=settingsStrList.pop()||"";const jsListStrRegExp=/\[[^\]]*\]/.exec(settingsStr);if(!jsListStrRegExp)return originData;const jsListStr=jsListStrRegExp[0];return{jsList:JSON.parse(jsListStr),settingsData:data.replace(jsListStr,"[]")}}/**
1
+ "use strict";const fs=require("fs");const path=require("path");const{load}=require("cheerio");const{deflate}=require("pako");const{getResourceMapper,getBase64FromFile}=require("./fs");const{TRANSPARENT_GIF}=require("./constants");const{applyObfuscation,isCocosBusinessScript,obfuscateLevelName}=require("../../utils/obfuscateLevels.js");const{omitPlayableWeight}=require("./omit-playable-weight");function readInject(rel){return fs.readFileSync(path.join(__dirname,rel),"utf8")}function paddingStyleTags($,originPkgPath,enableSplash){$("link[type=\"text/css\"]").toArray().forEach(item=>{const href=$(item).attr("href");if(!href)return;const cssPath=path.join(originPkgPath,href);if(!fs.existsSync(cssPath))return;const cssStr=fs.readFileSync(cssPath,"utf-8");$(`<style>${cssStr}</style>`).appendTo("head")});if(!enableSplash){$("<style>#splash,#splash .progress-bar{display:none!important;opacity:0;visibility:hidden}</style>").appendTo("head")}$("link[type=\"text/css\"]").remove();$("head").find("style").each((_index,elem)=>{const cssUrlReg=/url\("?'?.*"?'?\)/g;let styleTagStr=$(elem).html()||"";const matchStrList=styleTagStr.match(cssUrlReg);if(!matchStrList)return;matchStrList.forEach(str=>{const imgUrl=str.replace(/"|'|url|\(|\)/g,"");const imgPath=path.join(originPkgPath,imgUrl);const imgBase64=enableSplash&&fs.existsSync(imgPath)?getBase64FromFile(imgPath):TRANSPARENT_GIF;styleTagStr=styleTagStr.replace(str,`url(${imgBase64})`)});$(elem).html(styleTagStr)})}function paddingScriptTags($,originPkgPath){let scriptTags="";$("script[type=\"systemjs-importmap\"]").toArray().forEach(item=>{const href=$(item).attr("src");if(!href)return;const srcPath=path.join(originPkgPath,href);if(!fs.existsSync(srcPath))return;scriptTags+=`<script type="systemjs-importmap">${fs.readFileSync(srcPath,"utf-8")}</script>`});$("head link").remove();$("body script").remove();if(scriptTags)$(scriptTags).appendTo("body")}function getJsListFromSettingsJson(data){const jsonData=JSON.parse(data);jsonData.plugins={jsList:[],...jsonData.plugins};const jsList=[...jsonData.plugins.jsList];jsonData.plugins.jsList=[];return{jsList,settingsData:jsonData}}function stripSplashScreen(settingsData){if(!settingsData||!settingsData.splashScreen)return settingsData;settingsData.splashScreen.totalTime=0;settingsData.splashScreen.logo={type:"none",base64:""};return settingsData}function getJsListFromSettingsJs(data){const originData={jsList:[],settingsData:data};const settingsStrList=data.split("jsList:");if(settingsStrList.length<2)return originData;const settingsStr=settingsStrList.pop()||"";const jsListStrRegExp=/\[[^\]]*\]/.exec(settingsStr);if(!jsListStrRegExp)return originData;const jsListStr=jsListStrRegExp[0];return{jsList:JSON.parse(jsListStr),settingsData:data.replace(jsListStr,"[]")}}/**
2
+ * Keep an inlined JSON value from closing the surrounding script tag.
3
+ * `<` is only escaped inside JSON strings, so `\\u003c` is still valid JSON/JS.
4
+ * @param {string} json
5
+ */function escapeInlineScriptJson(json){return String(json).replace(/</g,"\\u003c").replace(/\u2028/g,"\\u2028").replace(/\u2029/g,"\\u2029")}/**
2
6
  * Inline a Creator 3.x web-mobile folder into a single HTML file.
7
+ * The resource map stays plain JSON. fflate compresses the finished HTML.
8
+ * Pass `isZip: true` only to force the legacy pako blob.
3
9
  * @param {string} originPkgPath - Absolute path to build/web-mobile
4
10
  * @param {string} singleFilePath - Output html path
5
- * @param {{ enableSplash?: boolean, isZip?: boolean, obfuscateLevel?: number }} [opts]
6
- */async function mergeWebMobile(originPkgPath,singleFilePath,opts={}){const enableSplash=opts.enableSplash===true;const isZip=opts.isZip!==false;const obfuscateLevel=Number(opts.obfuscateLevel)||0;if(obfuscateLevel>0){console.log(`[cocos] obfuscate business scripts level ${obfuscateLevel} (${obfuscateLevelName(obfuscateLevel)})`)}const htmlPath=path.join(originPkgPath,"index.html");if(!fs.existsSync(htmlPath)){throw new Error(`[cocos] web-mobile index.html not found: ${htmlPath}`)}const $=load(fs.readFileSync(htmlPath,"utf-8"));paddingStyleTags($,originPkgPath,enableSplash);paddingScriptTags($,originPkgPath);let pluginJsList=[];const{resMapper}=await getResourceMapper({dirPath:originPkgPath,rmHttp:true,mountCbFn:async(objKey,data)=>{if(objKey.indexOf("src/settings.json")!==-1){const{jsList,settingsData}=getJsListFromSettingsJson(data);pluginJsList=jsList;if(!enableSplash)stripSplashScreen(settingsData);return JSON.stringify(settingsData)}if(objKey.indexOf("src/settings.js")!==-1){const{jsList,settingsData}=getJsListFromSettingsJs(data);pluginJsList=jsList;return settingsData}if(obfuscateLevel>0&&isCocosBusinessScript(objKey)&&typeof data==="string"){const next=await applyObfuscation(data,obfuscateLevel);console.log(`[cocos] ${objKey} ${data.length} -> ${next.length} bytes`);return next}return data}});let resStr=JSON.stringify(resMapper);let compDiff=0;if(isZip){const zip=deflate(resStr);const zipStr=Buffer.from(zip).toString("base64");console.log("[cocos] origin pkg",resStr.length,"compressed",zipStr.length);if(zipStr.length<resStr.length){compDiff=resStr.length-zipStr.length;resStr=zipStr}}if(compDiff>0){$(`<script data-id="jszip">${readInject("injects/libs/pako.js")}</script>`).appendTo("body");$(`<script data-id="adapter-zip-0">window.__adapter_zip__="${resStr}";</script>`).appendTo("body")}else{$(`<script data-id="adapter-resource-0">window.__adapter_resource__=${resStr}</script>`).appendTo("body")}$(`<script data-id="adapter-plugins">window.__adapter_plugins__=${JSON.stringify(pluginJsList)}</script>`).appendTo("body");$(`<script data-id="adapter-init">${readInject("injects/3x/init.js")}</script>`).appendTo("body");$(`<script data-id="adapter-main">${readInject("injects/3x/main.js")}</script>`).appendTo("body");fs.mkdirSync(path.dirname(singleFilePath),{recursive:true});fs.writeFileSync(singleFilePath,$.html());const kb=(fs.statSync(singleFilePath).size/1024).toFixed(1);console.log(`[cocos] single-file template ${kb}kb → ${singleFilePath}`);return{resMapper,compDiff,singleFilePath}}module.exports={mergeWebMobile};
11
+ * @param {{ enableSplash?: boolean, isZip?: boolean, obfuscateLevel?: number, themeAssetNames?: string[] }} [opts]
12
+ */async function mergeWebMobile(originPkgPath,singleFilePath,opts={}){const enableSplash=opts.enableSplash===true;const isZip=opts.isZip===true;const obfuscateLevel=Number(opts.obfuscateLevel)||0;if(obfuscateLevel>0){console.log(`[cocos] obfuscate business scripts level ${obfuscateLevel} (${obfuscateLevelName(obfuscateLevel)})`)}const htmlPath=path.join(originPkgPath,"index.html");if(!fs.existsSync(htmlPath)){throw new Error(`[cocos] web-mobile index.html not found: ${htmlPath}`)}const $=load(fs.readFileSync(htmlPath,"utf-8"));paddingStyleTags($,originPkgPath,enableSplash);paddingScriptTags($,originPkgPath);let pluginJsList=[];const{resMapper}=await getResourceMapper({dirPath:originPkgPath,rmHttp:true,mountCbFn:async(objKey,data)=>{if(objKey.indexOf("src/settings.json")!==-1){const{jsList,settingsData}=getJsListFromSettingsJson(data);pluginJsList=jsList;if(!enableSplash)stripSplashScreen(settingsData);return JSON.stringify(settingsData)}if(objKey.indexOf("src/settings.js")!==-1){const{jsList,settingsData}=getJsListFromSettingsJs(data);pluginJsList=jsList;return settingsData}if(obfuscateLevel>0&&isCocosBusinessScript(objKey)&&typeof data==="string"){const next=await applyObfuscation(data,obfuscateLevel);console.log(`[cocos] ${objKey} ${data.length} -> ${next.length} bytes`);return next}return data}});const omitted=omitPlayableWeight(resMapper,{themeAssetNames:opts.themeAssetNames||[]});if(omitted.length){const bytes=omitted.reduce((sum,item)=>sum+item.bytes,0);console.log(`[cocos] omitted ${omitted.length} packed files, ${bytes} resource-map bytes`);for(const item of omitted)console.log(`[cocos] omit ${item.key}`)}let resStr=JSON.stringify(resMapper);let compDiff=0;if(isZip){const zip=deflate(resStr);const zipStr=Buffer.from(zip).toString("base64");console.log("[cocos] origin pkg",resStr.length,"compressed",zipStr.length);if(zipStr.length<resStr.length){compDiff=resStr.length-zipStr.length;resStr=zipStr}}else{console.log(`[cocos] inline resource map ${resStr.length} bytes (outer fflate compresses the html)`)}if(compDiff>0){$(`<script data-id="jszip">${readInject("injects/libs/pako.js")}</script>`).appendTo("body");$(`<script data-id="adapter-zip-0">window.__adapter_zip__="${resStr}";</script>`).appendTo("body")}else{const inline=escapeInlineScriptJson(resStr);$("<script data-id=\"adapter-resource-0\"></script>").text(`window.__adapter_resource__=${inline}`).appendTo("body")}$(`<script data-id="adapter-plugins">window.__adapter_plugins__=${JSON.stringify(pluginJsList)}</script>`).appendTo("body");$(`<script data-id="adapter-init">${readInject("injects/3x/init.js")}</script>`).appendTo("body");$(`<script data-id="adapter-main">${readInject("injects/3x/main.js")}</script>`).appendTo("body");fs.mkdirSync(path.dirname(singleFilePath),{recursive:true});fs.writeFileSync(singleFilePath,$.html());const kb=(fs.statSync(singleFilePath).size/1024).toFixed(1);console.log(`[cocos] single-file template ${kb}kb → ${singleFilePath}`);return{resMapper,compDiff,singleFilePath}}module.exports={mergeWebMobile,escapeInlineScriptJson};
@@ -0,0 +1,10 @@
1
+ "use strict";const fs=require("fs");const path=require("path");const{THEME_ASSET_EXT}=require("./constants");const BASE64_KEYS="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";const ASCII_TO_64=new Array(128).fill(0);for(let i=0;i<64;i++)ASCII_TO_64[BASE64_KEYS.charCodeAt(i)]=i;const HEX_CHARS="0123456789abcdef";const UUID_INDICES=[0,1,2,3,4,5,6,7,9,10,11,12,14,15,16,17,19,20,21,22,24,25,26,27,28,29,30,31,32,33,34,35];function decodeUuid(value){const head=String(value||"").split("@")[0];if(head.length!==22)return head;const tpl=new Array(36);tpl[0]=head[0];tpl[1]=head[1];tpl[8]="-";tpl[13]="-";tpl[18]="-";tpl[23]="-";let j=2;for(let i=2;i<22;i+=2){const lhs=ASCII_TO_64[head.charCodeAt(i)];const rhs=ASCII_TO_64[head.charCodeAt(i+1)];tpl[UUID_INDICES[j++]]=HEX_CHARS[lhs>>2];tpl[UUID_INDICES[j++]]=HEX_CHARS[(lhs&3)<<2|rhs>>4];tpl[UUID_INDICES[j++]]=HEX_CHARS[rhs&15]}return tpl.join("")}function resourceStem(logicalPath){const parts=String(logicalPath||"").split("/");if(parts[0]!=="textures"&&parts[0]!=="audio")return"";return parts[1]||""}function listThemeAssetNames(themeAssetsDir){if(!themeAssetsDir||!fs.existsSync(themeAssetsDir))return[];return fs.readdirSync(themeAssetsDir).filter(name=>THEME_ASSET_EXT.test(name)).map(name=>name.replace(/\.[^.]+$/,""))}function themeNativeUuids(config,themeAssetNames){const names=themeAssetNames instanceof Set?themeAssetNames:new Set(themeAssetNames||[]);const uuids=new Set;if(!config||!config.paths||!config.uuids||names.size===0)return uuids;for(const[idx,entry]of Object.entries(config.paths)){const stem=resourceStem(entry&&entry[0]);if(!stem||!names.has(stem))continue;const decoded=decodeUuid(config.uuids[Number(idx)]);if(decoded)uuids.add(decoded)}return uuids}// Minimal TTF (one .notdef and a space). FontFace accepts it, so builtin font
2
+ // preload still completes. The game does not reference the OpenSans family.
3
+ const STUB_FONT_DATA_URL="data:font/ttf;base64,AAEAAAAKAIAAAwAgT1MvMkEXQVgAAAEoAAAAYGNtYXAADABzAAABkAAAADRnbHlmAAAAAAAAAcwAAAABaGVhZF8WQOAAAACsAAAANmhoZWEDIgEuAAAA5AAAACRobXR4AfQAAAAAAYgAAAAGbG9jYQAAAAAAAAHEAAAABm1heHAAAwACAAABCAAAACBuYW1lYDp5eQAAAdAAAABXcG9zdAAHAAAAAAIoAAAAJgABAAAAAQAAmnCii18PPPUAAwPoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwACAAAAAAAAAAEAAAMg/zgAAAH0AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAEAAAACAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAwH0AZAABQAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAPz8/PwAAACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAB9AAAAAAAAAAAAAIAAAADAAAAFAADAAEAAAAUAAQAIAAAAAQABAABAAAAIP//AAAAIP///+EAAQAAAAAAAAAAAAAAAAAAAAAAAAAEADYAAQAAAAAAAQAEAAAAAQAAAAAAAgAHAAQAAwABBAkAAQAIAAsAAwABBAkAAgAOABNTdHViUmVndWxhcgBTAHQAdQBiAFIAZQBnAHUAbABhAHIAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAMAAA==";function stubBuiltinFonts(resMapper){const stubbed=[];for(const key of Object.keys(resMapper)){if(!/OpenSans-[^/]+\.ttf$/.test(key))continue;const value=resMapper[key];if(typeof value!=="string"||value===STUB_FONT_DATA_URL)continue;stubbed.push({key,bytes:value.length});resMapper[key]=STUB_FONT_DATA_URL}if(stubbed.length){const bytes=stubbed.reduce((sum,item)=>sum+item.bytes,0);console.log(`[cocos] stubbed ${stubbed.length} builtin fonts, ${bytes} resource-map bytes`);for(const item of stubbed)console.log(`[cocos] stub font ${item.key}`)}return stubbed}/**
4
+ * Drop resources natives that the theme already inlines into the business script.
5
+ * OpenSans entries stay, because builtinAssets loads them at startup, but the
6
+ * TTF bytes are replaced with a tiny font.
7
+ * Spine wasm stays: the engine imports it from onPostInfrastructureInitDelegate.
8
+ * Mutates resMapper. Does not touch build/web-mobile on disk.
9
+ * @returns {{ key: string, bytes: number }[]}
10
+ */function omitPlayableWeight(resMapper,options={}){const removed=[];const drop=key=>{if(!Object.prototype.hasOwnProperty.call(resMapper,key))return;const value=resMapper[key];removed.push({key,bytes:typeof value==="string"?value.length:0});delete resMapper[key]};const configRaw=resMapper["assets/resources/config.json"];if(typeof configRaw==="string"){let config=null;try{config=JSON.parse(configRaw)}catch{config=null}const uuids=themeNativeUuids(config,options.themeAssetNames);if(uuids.size){for(const key of Object.keys(resMapper)){if(!key.includes("assets/resources/native/"))continue;for(const uuid of uuids){if(key.includes(uuid)){drop(key);break}}}}}stubBuiltinFonts(resMapper);return removed}function themeAssetsDirForProject(projectRoot,themeName,themeConfig={}){const sourceDir=themeConfig.sourceDir||"src/theme";return path.join(projectRoot,sourceDir,themeName,"assets")}module.exports={STUB_FONT_DATA_URL,decodeUuid,listThemeAssetNames,omitPlayableWeight,resourceStem,stubBuiltinFonts,themeAssetsDirForProject,themeNativeUuids};
@@ -0,0 +1,2 @@
1
+ "use strict";const{describe,it}=require("node:test");const assert=require("node:assert/strict");const{STUB_FONT_DATA_URL,decodeUuid,omitPlayableWeight,resourceStem}=require("./omit-playable-weight.js");function configWith(paths,uuids){return JSON.stringify({paths,uuids})}describe("omitPlayableWeight",()=>{it("decodes a compressed Creator uuid",()=>{assert.equal(decodeUuid("d56fKxjDBLAq9Efh2aLDtY"),"d5e9f2b1-8c30-4b02-af44-7e1d9a2c3b58")});it("reads the asset stem and ignores effects",()=>{assert.equal(resourceStem("textures/end_logo/spriteFrame"),"end_logo");assert.equal(resourceStem("audio/hole"),"hole");assert.equal(resourceStem("effects/worm-lit"),"")});it("drops theme-duplicated resource natives and replaces builtin fonts",()=>{const endLogo="d5e9f2b1-8c30-4b02-af44-7e1d9a2c3b58";const wormLit="d93adc7c-15c2-41f4-92fb-2fb4e0f07743";const resMapper={"assets/internal/native/08/uuid/OpenSans-Regular.ttf":"font-regular","assets/internal/native/b5/uuid/OpenSans-BoldItalic.ttf":"font-bi","assets/internal/config.json":"{\"paths\":{}}",[`assets/resources/native/d5/${endLogo}.png`]:"logo-bytes",[`assets/resources/import/d9/${wormLit}.json`]:"shader","assets/resources/native/6b/6bb1ac2c-95e2-4505-a143-e11b554b15a6.mp3":"hole","assets/resources/native/aa/unrelated.png":"keep-me","cocos-js/spine-BZAdhYHP.js":"spine-js","cocos-js/assets/spine-BsTbhDIS.wasm":"wasm","cocos-js/_virtual_cc.js":"engine","src/import-map.json":"{\"imports\":{\"cc\":\"./../cocos-js/cc.js\"}}"};// The first uuid in this fixture is not a real compressed form; use the decoded uuid directly.
2
+ resMapper["assets/resources/config.json"]=configWith({0:["textures/end_logo/texture",3,1],1:["effects/worm-lit",0,1],2:["audio/hole",1,1],3:["textures/brain",2,1]},[endLogo,wormLit,"6bb1ac2c-95e2-4505-a143-e11b554b15a6","aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"]);const removed=omitPlayableWeight(resMapper,{themeAssetNames:["end_logo","hole","brain"]});const keys=removed.map(item=>item.key).sort();assert.deepEqual(keys,[`assets/resources/native/d5/${endLogo}.png`,"assets/resources/native/6b/6bb1ac2c-95e2-4505-a143-e11b554b15a6.mp3"].sort());assert.equal(resMapper["assets/internal/native/08/uuid/OpenSans-Regular.ttf"],STUB_FONT_DATA_URL);assert.equal(resMapper["assets/internal/native/b5/uuid/OpenSans-BoldItalic.ttf"],STUB_FONT_DATA_URL);assert.equal(resMapper[`assets/resources/import/d9/${wormLit}.json`],"shader");assert.equal(resMapper["assets/resources/native/aa/unrelated.png"],"keep-me");assert.equal(resMapper["cocos-js/spine-BZAdhYHP.js"],"spine-js");assert.equal(resMapper["cocos-js/assets/spine-BsTbhDIS.wasm"],"wasm");assert.equal(resMapper["cocos-js/_virtual_cc.js"],"engine");assert.ok(!Object.keys(resMapper).some(key=>key.includes("aaaaaaaa")))})});
@@ -1 +1 @@
1
- "use strict";const fs=require("fs");const path=require("path");const{options}=require("../../options.js");const{mergeOptions}=require("../../utils/mergeOptions.js");const{logOptions}=require("../../utils/logOptions.js");const{resolveCreatorOutputDir}=require("./creator-cli");const{runCocosEngineBuild}=require("./cli-build");const{rebundlePreviewMerge}=require("./rebundle-preview.js");const{mergeWebMobile}=require("./merge");const{detectCurrentTheme,loadThemePayload,writeGeneratedThemeModule}=require("./theme-assets");const{emitChannel}=require("../../artifacts/emit");const{isSocialMediaNetwork,stripSocialShareFromDir}=require("./strip-social-share");function resolveWebMobileDir(root,buildOptions){const cocos=buildOptions.cocos||{};const fromCreator=resolveCreatorOutputDir(root,cocos.buildConfig||"build-configs/web-mobile.json");const explicit=cocos.webMobileDir?path.resolve(root,cocos.webMobileDir):null;if(cocos.skipEngineBuild){return explicit||fromCreator||path.resolve(root,"dist/web-mobile")}if(fromCreator){if(explicit&&explicit!==fromCreator){console.warn(`[cocos] webMobileDir (${path.relative(root,explicit)}) != Creator output (${path.relative(root,fromCreator)}); using Creator buildPath/outputName`)}return fromCreator}return explicit||path.resolve(root,"dist/web-mobile")}async function mergeTemplate(root,buildOptions){const webMobileDir=resolveWebMobileDir(root,buildOptions);const singleFilePath=path.join(root,"build","single-file-3x.html");await mergeWebMobile(webMobileDir,singleFilePath,{enableSplash:!!(buildOptions.cocos&&buildOptions.cocos.enableSplash),obfuscateLevel:buildOptions.obfuscateLevel});return fs.readFileSync(singleFilePath,"utf8")}function resolveThemeName(root,buildOptions,themeConfig){return buildOptions.themeName||detectCurrentTheme(root,themeConfig)||"default"}function loadBuildsThemeConfig(root){const configPath=path.join(root,"builds.config.js");if(!fs.existsSync(configPath))return{sourceDir:"src/theme",entryFile:"src/theme/index.ts"};try{delete require.cache[configPath];const config=require(configPath);return{sourceDir:"src/theme",entryFile:"src/theme/index.ts",...(config.themes||{})}}catch{return{sourceDir:"src/theme",entryFile:"src/theme/index.ts"}}}function stripPackedChannel(outDir,network){if(isSocialMediaNetwork(network)){console.log("[cocos] social_media: keep AiX hooks (inject IIFE later)");return}const packed=stripSocialShareFromDir(outDir);console.log(`[cocos] stripped social-share from ${packed.changed}/${packed.files} files in ${path.relative(process.cwd(),outDir)||"."}`)}async function prepareCocosTemplate(customOptions){const buildOptions=mergeOptions(options,customOptions);const root=process.cwd();await runCocosEngineBuild(root,buildOptions);await rebundlePreviewMerge(root);return mergeTemplate(root,buildOptions)}async function emitCocosChannel(templateHtml,customOptions){const buildOptions=mergeOptions(options,customOptions);const root=process.cwd();const themeConfig=loadBuildsThemeConfig(root);const themeName=resolveThemeName(root,buildOptions,themeConfig);let themePayload={themeName,mergedData:{},i18nOverride:{},assetRecordByPath:{}};try{themePayload=loadThemePayload(root,themeName,themeConfig)}catch(err){console.warn(`[cocos] theme payload skipped: ${err.message}`)}const generated=buildOptions.cocos&&buildOptions.cocos.generatedThemeFile||"assets/scripts/platform/theme.generated.ts";writeGeneratedThemeModule(path.join(root,generated),themePayload);const outDir=path.resolve(root,buildOptions.outDir);await emitChannel({templateHtml,buildOptions,themePayload,themeConfig,outDir});stripPackedChannel(outDir,buildOptions.network)}async function runCocosBuild(customOptions){const buildOptions=mergeOptions(options,customOptions);logOptions(buildOptions);const templateHtml=await prepareCocosTemplate(buildOptions);await emitCocosChannel(templateHtml,customOptions)}async function packCocosBatch({config,themes,tasks,outputDir,projectRoot,isChannelFold2Zip}){const buildOptionsBase=mergeOptions(options,{engine:"cocos",googlePlayUrl:config.googlePlayUrl,appStoreUrl:config.appStoreUrl,isChannelFold2Zip,obfuscateLevel:config.obfuscateLevel,fflateCompression:config.fflateCompression});if(config.cocos)Object.assign(buildOptionsBase.cocos,config.cocos);if(buildOptionsBase.cocos.cleanCache!==false&&process.env.PLAYABLE_KEEP_COCOS_CACHE!=="1"){buildOptionsBase.cocos.cleanCache=true}await runCocosEngineBuild(projectRoot,buildOptionsBase);await rebundlePreviewMerge(projectRoot);const templateHtml=await mergeTemplate(projectRoot,buildOptionsBase);const themeConfig=config.themes||loadBuildsThemeConfig(projectRoot);const results=[];for(const theme of themes){let themePayload;try{themePayload=loadThemePayload(projectRoot,theme,themeConfig)}catch(err){console.error(`[cocos] theme ${theme}: ${err.message}`);for(const task of tasks){results.push({success:false,theme,channel:task.channel,orientation:task.orientation,error:err.message})}continue}for(const task of tasks){const orientationValue=task.orientation||"both";const fileName=config.naming&&typeof config.naming.generator==="function"?config.naming.generator(theme,task.channel):`${theme}-${task.channel}`;const tempDir=path.join(projectRoot,`${config.build.tempDirPrefix||"dist-temp"}-cocos-${theme}-${task.channel}-${orientationValue}`);try{const channelOptions=mergeOptions(buildOptionsBase,{network:task.channel,playcraftOrientationValue:orientationValue,filename:fileName||options.filename,outDir:tempDir,isChannelFold2Zip});await emitChannel({templateHtml,buildOptions:channelOptions,themePayload,themeConfig,outDir:tempDir});stripPackedChannel(tempDir,task.channel);const themeDir=path.join(outputDir,theme);fs.mkdirSync(themeDir,{recursive:true});for(const file of fs.readdirSync(tempDir)){const src=path.join(tempDir,file);if(fs.statSync(src).isDirectory())continue;fs.copyFileSync(src,path.join(themeDir,file))}results.push({success:true,theme,channel:task.channel,orientation:orientationValue})}catch(error){results.push({success:false,theme,channel:task.channel,orientation:orientationValue,error:error.message})}finally{if(config.build.cleanTemp!==false&&fs.existsSync(tempDir)){fs.rmSync(tempDir,{recursive:true,force:true})}}}}return results}async function syncThemeForPreview(projectRoot,themeName,themeConfig,generatedRel){const payload=loadThemePayload(projectRoot,themeName,themeConfig);writeGeneratedThemeModule(path.join(projectRoot,generatedRel),payload);return payload}module.exports={runCocosBuild,prepareCocosTemplate,emitCocosChannel,packCocosBatch,mergeTemplate,syncThemeForPreview};
1
+ "use strict";const fs=require("fs");const path=require("path");const{options}=require("../../options.js");const{mergeOptions}=require("../../utils/mergeOptions.js");const{logOptions}=require("../../utils/logOptions.js");const{resolveCreatorOutputDir}=require("./creator-cli");const{runCocosEngineBuild}=require("./cli-build");const{rebundlePreviewMerge}=require("./rebundle-preview.js");const{mergeWebMobile}=require("./merge");const{detectCurrentTheme,loadThemePayload,writeGeneratedThemeModule}=require("./theme-assets");const{listThemeAssetNames,themeAssetsDirForProject}=require("./omit-playable-weight");const{emitChannel}=require("../../artifacts/emit");const{isSocialMediaNetwork,stripSocialShareFromDir}=require("./strip-social-share");function resolveWebMobileDir(root,buildOptions){const cocos=buildOptions.cocos||{};const fromCreator=resolveCreatorOutputDir(root,cocos.buildConfig||"build-configs/web-mobile.json");const explicit=cocos.webMobileDir?path.resolve(root,cocos.webMobileDir):null;if(cocos.skipEngineBuild){return explicit||fromCreator||path.resolve(root,"dist/web-mobile")}if(fromCreator){if(explicit&&explicit!==fromCreator){console.warn(`[cocos] webMobileDir (${path.relative(root,explicit)}) != Creator output (${path.relative(root,fromCreator)}); using Creator buildPath/outputName`)}return fromCreator}return explicit||path.resolve(root,"dist/web-mobile")}async function mergeTemplate(root,buildOptions){const webMobileDir=resolveWebMobileDir(root,buildOptions);const singleFilePath=path.join(root,"build","single-file-3x.html");const themeConfig=loadBuildsThemeConfig(root);const themeName=resolveThemeName(root,buildOptions,themeConfig);const themeAssetNames=listThemeAssetNames(themeAssetsDirForProject(root,themeName,themeConfig));await mergeWebMobile(webMobileDir,singleFilePath,{enableSplash:!!(buildOptions.cocos&&buildOptions.cocos.enableSplash),obfuscateLevel:buildOptions.obfuscateLevel,themeAssetNames,isZip:false});return fs.readFileSync(singleFilePath,"utf8")}function resolveThemeName(root,buildOptions,themeConfig){return buildOptions.themeName||detectCurrentTheme(root,themeConfig)||"default"}function loadBuildsPackConfig(root){const configPath=path.join(root,"builds.config.js");if(!fs.existsSync(configPath))return{};try{delete require.cache[configPath];return require(configPath)||{}}catch{return{}}}function cliSetsFflate(){return process.argv.slice(2).some(arg=>arg==="--fflate-compression"||arg.startsWith("--fflate-compression="))}/** builds.config.js owns fflate for Cocos unless the CLI flag is present. */function withBuildsPackFlags(root,buildOptions){const config=loadBuildsPackConfig(root);if(typeof config.fflateCompression!=="boolean"||cliSetsFflate())return buildOptions;return{...buildOptions,fflateCompression:config.fflateCompression}}function loadBuildsThemeConfig(root){const configPath=path.join(root,"builds.config.js");if(!fs.existsSync(configPath))return{sourceDir:"src/theme",entryFile:"src/theme/index.ts"};try{delete require.cache[configPath];const config=require(configPath);return{sourceDir:"src/theme",entryFile:"src/theme/index.ts",...(config.themes||{})}}catch{return{sourceDir:"src/theme",entryFile:"src/theme/index.ts"}}}function stripPackedChannel(outDir,network){if(isSocialMediaNetwork(network)){console.log("[cocos] social_media: keep AiX hooks (inject IIFE later)");return}const packed=stripSocialShareFromDir(outDir);console.log(`[cocos] stripped social-share from ${packed.changed}/${packed.files} files in ${path.relative(process.cwd(),outDir)||"."}`)}async function prepareCocosTemplate(customOptions){const root=process.cwd();const buildOptions=withBuildsPackFlags(root,mergeOptions(options,customOptions));await runCocosEngineBuild(root,buildOptions);await rebundlePreviewMerge(root);return mergeTemplate(root,buildOptions)}async function emitCocosChannel(templateHtml,customOptions){const root=process.cwd();const buildOptions=withBuildsPackFlags(root,mergeOptions(options,customOptions));const themeConfig=loadBuildsThemeConfig(root);const themeName=resolveThemeName(root,buildOptions,themeConfig);let themePayload={themeName,mergedData:{},i18nOverride:{},assetRecordByPath:{}};try{themePayload=loadThemePayload(root,themeName,themeConfig)}catch(err){console.warn(`[cocos] theme payload skipped: ${err.message}`)}const generated=buildOptions.cocos&&buildOptions.cocos.generatedThemeFile||"assets/scripts/platform/theme.generated.ts";writeGeneratedThemeModule(path.join(root,generated),themePayload);const outDir=path.resolve(root,buildOptions.outDir);await emitChannel({templateHtml,buildOptions,themePayload,themeConfig,outDir});stripPackedChannel(outDir,buildOptions.network)}async function runCocosBuild(customOptions){const buildOptions=withBuildsPackFlags(process.cwd(),mergeOptions(options,customOptions));logOptions(buildOptions);const templateHtml=await prepareCocosTemplate(buildOptions);await emitCocosChannel(templateHtml,customOptions)}async function packCocosBatch({config,themes,tasks,outputDir,projectRoot,isChannelFold2Zip}){const buildOptionsBase=mergeOptions(options,{engine:"cocos",googlePlayUrl:config.googlePlayUrl,appStoreUrl:config.appStoreUrl,isChannelFold2Zip,obfuscateLevel:config.obfuscateLevel,fflateCompression:config.fflateCompression});if(config.cocos)Object.assign(buildOptionsBase.cocos,config.cocos);if(buildOptionsBase.cocos.cleanCache!==false&&process.env.PLAYABLE_KEEP_COCOS_CACHE!=="1"){buildOptionsBase.cocos.cleanCache=true}await runCocosEngineBuild(projectRoot,buildOptionsBase);await rebundlePreviewMerge(projectRoot);const templateHtml=await mergeTemplate(projectRoot,buildOptionsBase);const themeConfig=config.themes||loadBuildsThemeConfig(projectRoot);const results=[];for(const theme of themes){let themePayload;try{themePayload=loadThemePayload(projectRoot,theme,themeConfig)}catch(err){console.error(`[cocos] theme ${theme}: ${err.message}`);for(const task of tasks){results.push({success:false,theme,channel:task.channel,orientation:task.orientation,error:err.message})}continue}for(const task of tasks){const orientationValue=task.orientation||"both";const fileName=config.naming&&typeof config.naming.generator==="function"?config.naming.generator(theme,task.channel):`${theme}-${task.channel}`;const tempDir=path.join(projectRoot,`${config.build.tempDirPrefix||"dist-temp"}-cocos-${theme}-${task.channel}-${orientationValue}`);try{const channelOptions=mergeOptions(buildOptionsBase,{network:task.channel,playcraftOrientationValue:orientationValue,filename:fileName||options.filename,outDir:tempDir,isChannelFold2Zip});await emitChannel({templateHtml,buildOptions:channelOptions,themePayload,themeConfig,outDir:tempDir});stripPackedChannel(tempDir,task.channel);const themeDir=path.join(outputDir,theme);fs.mkdirSync(themeDir,{recursive:true});for(const file of fs.readdirSync(tempDir)){const src=path.join(tempDir,file);if(fs.statSync(src).isDirectory())continue;fs.copyFileSync(src,path.join(themeDir,file))}results.push({success:true,theme,channel:task.channel,orientation:orientationValue})}catch(error){results.push({success:false,theme,channel:task.channel,orientation:orientationValue,error:error.message})}finally{if(config.build.cleanTemp!==false&&fs.existsSync(tempDir)){fs.rmSync(tempDir,{recursive:true,force:true})}}}}return results}async function syncThemeForPreview(projectRoot,themeName,themeConfig,generatedRel){const payload=loadThemePayload(projectRoot,themeName,themeConfig);writeGeneratedThemeModule(path.join(projectRoot,generatedRel),payload);return payload}module.exports={runCocosBuild,prepareCocosTemplate,emitCocosChannel,packCocosBatch,mergeTemplate,syncThemeForPreview};
@@ -1,8 +1,9 @@
1
- "use strict";const crypto=require("crypto");const fs=require("fs");const path=require("path");const ENTRY="cce:/internal/x/prerequisite-imports";const SKIP=new Set(["cce:/internal/x/cc"]);const GLOBAL_DEPS=new Set(["cc","cc/env","cc/userland/macro"]);const BUNDLE_FILE="playcraft-bundle.js";const BUNDLE_URL=`./chunks/${BUNDLE_FILE}`;const REGISTER_RE=/^System\.register\(\s*(\[[\s\S]*?\])\s*,\s*function/;function previewDir(projectRoot){return path.join(projectRoot,"temp/programming/packer-driver/targets/preview")}function normalizeChunkUrl(url){const raw=String(url||"").replace(/\\/g,"/");if(raw.startsWith("./chunks/"))return raw;if(raw.startsWith("chunks/"))return`./${raw}`;return raw}function isChunkUrl(url){return normalizeChunkUrl(url).startsWith("./chunks/")&&normalizeChunkUrl(url).endsWith(".js")}function packChunkName(chunkUrl){return`pack-chunk:${path.basename(normalizeChunkUrl(chunkUrl),".js")}`}function shortHash(buf){return crypto.createHash("sha1").update(buf).digest("hex").slice(0,10)}function isBundledImportMap(map){const imports=map&&map.imports||{};const entry=String(imports[ENTRY]||"").split("?")[0];return entry===BUNDLE_URL&&Object.keys(imports).length===1}function loadSourceImportMap(dir){const livePath=path.join(dir,"import-map.json");const backupPath=path.join(dir,"import-map.quickpack.json");if(!fs.existsSync(livePath))return null;const live=JSON.parse(fs.readFileSync(livePath,"utf8"));if(isBundledImportMap(live)){if(!fs.existsSync(backupPath))return null;return JSON.parse(fs.readFileSync(backupPath,"utf8"))}fs.writeFileSync(backupPath,`${JSON.stringify(live,null,2)}\n`);return live}function skipChunkUrls(map){const skip=new Set;const imports=map.imports||{};for(const spec of SKIP){if(isChunkUrl(imports[spec]))skip.add(normalizeChunkUrl(imports[spec]))}return skip}function collectChunkUrls(map){const skipUrls=skipChunkUrls(map);const urls=new Set;const queue=[];const add=url=>{if(!isChunkUrl(url))return;const normalized=normalizeChunkUrl(url);if(skipUrls.has(normalized)||urls.has(normalized))return;urls.add(normalized);queue.push(normalized)};const imports=map.imports||{};for(const[spec,url]of Object.entries(imports)){if(SKIP.has(spec))continue;add(url)}while(queue.length){const scopeUrl=queue.shift();for(const target of Object.values((map.scopes||{})[scopeUrl]||{})){add(target)}}return urls}function rewriteDep(dep,chunkUrl,map,nameOf){if(GLOBAL_DEPS.has(dep))return dep;const mapped=((map.scopes||{})[chunkUrl]||{})[dep]||(map.imports||{})[dep];if(!mapped)return dep;if(typeof mapped==="string"&&mapped.startsWith("cce:/internal/x/cc-fu"))return"cc";if(SKIP.has(mapped)||mapped===(map.imports||{})["cce:/internal/x/cc"])return"cc";if(isChunkUrl(mapped))return nameOf(normalizeChunkUrl(mapped));return mapped}function rewriteChunk(source,name,chunkUrl,map,nameOf,anonymous){const stripped=source.replace(/\n\/\/# sourceMappingURL=.*$/m,"");const match=stripped.match(REGISTER_RE);if(!match){throw new Error(`no System.register in ${chunkUrl}`)}const deps=JSON.parse(match[1]).map(dep=>rewriteDep(dep,chunkUrl,map,nameOf));const head=anonymous?`System.register(${JSON.stringify(deps)}, function`:`System.register(${JSON.stringify(name)}, ${JSON.stringify(deps)}, function`;return`${head}${stripped.slice(match[0].length)}`}/**
1
+ "use strict";const crypto=require("crypto");const fs=require("fs");const path=require("path");const ENTRY="cce:/internal/x/prerequisite-imports";const SKIP=new Set(["cce:/internal/x/cc"]);const GLOBAL_DEPS=new Set(["cc","cc/env","cc/userland/macro"]);const BUNDLE_FILE="playcraft-bundle.js";const BUNDLE_URL=`./chunks/${BUNDLE_FILE}`;const EMPTY_NAME="pack-chunk:empty";const EMPTY_REGISTER=`System.register(${JSON.stringify(EMPTY_NAME)}, [], function (_export, _context) {\n`+" return { setters: [], execute() {} };\n"+"});";const REGISTER_RE=/^System\.register\(\s*(\[[\s\S]*?\])\s*,\s*function/;const UNRESOLVED_RE=/^__unresolved_\d+$/;function previewDir(projectRoot){return path.join(projectRoot,"temp/programming/packer-driver/targets/preview")}function normalizeChunkUrl(url){const raw=String(url||"").replace(/\\/g,"/");if(raw.startsWith("./chunks/"))return raw;if(raw.startsWith("chunks/"))return`./${raw}`;return raw}function isChunkUrl(url){return normalizeChunkUrl(url).startsWith("./chunks/")&&normalizeChunkUrl(url).endsWith(".js")}function packChunkName(chunkUrl){return`pack-chunk:${path.basename(normalizeChunkUrl(chunkUrl),".js")}`}function shortHash(buf){return crypto.createHash("sha1").update(buf).digest("hex").slice(0,10)}function isBundledImportMap(map){const imports=map&&map.imports||{};const entry=String(imports[ENTRY]||"").split("?")[0];return entry===BUNDLE_URL&&Object.keys(imports).length===1}function loadSourceImportMap(dir){const livePath=path.join(dir,"import-map.json");const backupPath=path.join(dir,"import-map.quickpack.json");if(!fs.existsSync(livePath))return null;const live=JSON.parse(fs.readFileSync(livePath,"utf8"));if(isBundledImportMap(live)){if(!fs.existsSync(backupPath))return null;return JSON.parse(fs.readFileSync(backupPath,"utf8"))}fs.writeFileSync(backupPath,`${JSON.stringify(live,null,2)}\n`);return live}function skipChunkUrls(map){const skip=new Set;const imports=map.imports||{};for(const spec of SKIP){if(isChunkUrl(imports[spec]))skip.add(normalizeChunkUrl(imports[spec]))}return skip}function collectChunkUrls(map){const skipUrls=skipChunkUrls(map);const urls=new Set;const queue=[];const add=url=>{if(!isChunkUrl(url))return;const normalized=normalizeChunkUrl(url);if(skipUrls.has(normalized)||urls.has(normalized))return;urls.add(normalized);queue.push(normalized)};const imports=map.imports||{};for(const[spec,url]of Object.entries(imports)){if(SKIP.has(spec))continue;add(url)}while(queue.length){const scopeUrl=queue.shift();for(const target of Object.values((map.scopes||{})[scopeUrl]||{})){add(target)}}return urls}function rewriteDep(dep,chunkUrl,map,nameOf){if(GLOBAL_DEPS.has(dep))return dep;const mapped=((map.scopes||{})[chunkUrl]||{})[dep]||(map.imports||{})[dep];if(!mapped){// QuickPack leaves extra __unresolved_N after scripts are deleted.
2
+ return UNRESOLVED_RE.test(dep)?EMPTY_NAME:dep}if(typeof mapped==="string"&&mapped.startsWith("cce:/internal/x/cc-fu"))return"cc";if(SKIP.has(mapped)||mapped===(map.imports||{})["cce:/internal/x/cc"])return"cc";if(isChunkUrl(mapped))return nameOf(normalizeChunkUrl(mapped));return mapped}function rewriteChunk(source,name,chunkUrl,map,nameOf,anonymous){const stripped=source.replace(/\n\/\/# sourceMappingURL=.*$/m,"");const match=stripped.match(REGISTER_RE);if(!match){throw new Error(`no System.register in ${chunkUrl}`)}const deps=JSON.parse(match[1]).map(dep=>rewriteDep(dep,chunkUrl,map,nameOf));const head=anonymous?`System.register(${JSON.stringify(deps)}, function`:`System.register(${JSON.stringify(name)}, ${JSON.stringify(deps)}, function`;return`${head}${stripped.slice(match[0].length)}`}/**
2
3
  * Fold QuickPack's per-file chunks into one named-register bundle and
3
4
  * point prerequisite-imports at it. Original chunks stay on disk for
4
5
  * incremental compile; import-map.quickpack.json keeps the pre-rewrite map.
5
6
  */function concatPreviewScripts(projectRoot){const dir=previewDir(projectRoot);const map=loadSourceImportMap(dir);if(!map)return null;const imports=map.imports||{};const chunkToSpecifier=new Map;for(const[spec,url]of Object.entries(imports)){if(SKIP.has(spec)||!isChunkUrl(url))continue;chunkToSpecifier.set(normalizeChunkUrl(url),spec)}const entryChunk=imports[ENTRY]&&isChunkUrl(imports[ENTRY])?normalizeChunkUrl(imports[ENTRY]):null;const nameOf=chunkUrl=>{if(entryChunk&&chunkUrl===entryChunk)return ENTRY;return chunkToSpecifier.get(chunkUrl)||packChunkName(chunkUrl)};const chunkUrls=[...collectChunkUrls(map)];if(!chunkUrls.length)return null;// Named modules first; anonymous entry last so script-tag onload getRegister()
6
7
  // binds prerequisite-imports (named-register's firstNamedDefine is cleared
7
8
  // before the load event).
8
- const ordered=[];for(const url of chunkUrls){if(url!==entryChunk)ordered.push(url)}if(entryChunk&&chunkUrls.includes(entryChunk))ordered.push(entryChunk);const parts=[];for(const chunkUrl of ordered){const abs=path.join(dir,chunkUrl.replace(/^\.\//,""));if(!fs.existsSync(abs)){throw new Error(`missing chunk ${chunkUrl}`)}parts.push(rewriteChunk(fs.readFileSync(abs,"utf8"),nameOf(chunkUrl),chunkUrl,map,nameOf,chunkUrl===entryChunk))}const bundle=`${parts.join("\n")}\n`;const bundleAbs=path.join(dir,"chunks",BUNDLE_FILE);fs.mkdirSync(path.dirname(bundleAbs),{recursive:true});fs.writeFileSync(bundleAbs,bundle);const hashedUrl=`${BUNDLE_URL}?h=${shortHash(bundle)}`;const bundledMap={imports:{[ENTRY]:hashedUrl}};const tmp=path.join(dir,"import-map.json.tmp");fs.writeFileSync(tmp,`${JSON.stringify(bundledMap)}\n`);fs.renameSync(tmp,path.join(dir,"import-map.json"));return{modules:ordered.length,bytes:Buffer.byteLength(bundle),bundle:hashedUrl}}module.exports={concatPreviewScripts,ENTRY,BUNDLE_URL,BUNDLE_FILE,isBundledImportMap};
9
+ const ordered=[];for(const url of chunkUrls){if(url!==entryChunk)ordered.push(url)}if(entryChunk&&chunkUrls.includes(entryChunk))ordered.push(entryChunk);const parts=[EMPTY_REGISTER];for(const chunkUrl of ordered){const abs=path.join(dir,chunkUrl.replace(/^\.\//,""));if(!fs.existsSync(abs)){throw new Error(`missing chunk ${chunkUrl}`)}parts.push(rewriteChunk(fs.readFileSync(abs,"utf8"),nameOf(chunkUrl),chunkUrl,map,nameOf,chunkUrl===entryChunk))}const bundle=`${parts.join("\n")}\n`;const bundleAbs=path.join(dir,"chunks",BUNDLE_FILE);fs.mkdirSync(path.dirname(bundleAbs),{recursive:true});fs.writeFileSync(bundleAbs,bundle);const hashedUrl=`${BUNDLE_URL}?h=${shortHash(bundle)}`;const bundledMap={imports:{[ENTRY]:hashedUrl}};const tmp=path.join(dir,"import-map.json.tmp");fs.writeFileSync(tmp,`${JSON.stringify(bundledMap)}\n`);fs.renameSync(tmp,path.join(dir,"import-map.json"));return{modules:ordered.length,bytes:Buffer.byteLength(bundle),bundle:hashedUrl}}module.exports={concatPreviewScripts,ENTRY,BUNDLE_URL,BUNDLE_FILE,isBundledImportMap};
@@ -1 +1 @@
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{concatPreviewScripts,ENTRY}=require("./preview-concat.js");describe("concatPreviewScripts",()=>{let root;let preview;before(()=>{root=fs.mkdtempSync(path.join(os.tmpdir(),"preview-concat-"));preview=path.join(root,"temp/programming/packer-driver/targets/preview");fs.mkdirSync(path.join(preview,"chunks/aa"),{recursive:true});fs.mkdirSync(path.join(preview,"chunks/bb"),{recursive:true});fs.writeFileSync(path.join(preview,"chunks/aa/aaa111.js"),"System.register([\"__unresolved_0\", \"cc\", \"@playcraft/adsdk/cjs\"], function (_export, _context) {\n return { setters: [], execute() {} };\n});\n//# sourceMappingURL=aaa111.js.map\n");fs.writeFileSync(path.join(preview,"chunks/bb/bbb222.js"),"System.register([], function (_export, _context) {\n return { setters: [], execute() {} };\n});\n");fs.writeFileSync(path.join(preview,"import-map.json"),JSON.stringify({imports:{[ENTRY]:"./chunks/aa/aaa111.js","cce:/internal/x/cc":"./chunks/cc/skip-me.js"},scopes:{"./chunks/aa/aaa111.js":{__unresolved_0:"./chunks/bb/bbb222.js","@playcraft/adsdk/cjs":"./chunks/bb/bbb222.js",cc:"./chunks/cc/skip-me.js"}}}))});after(()=>{fs.rmSync(root,{recursive:true,force:true})});it("writes one named-register bundle and a single import-map entry",()=>{const result=concatPreviewScripts(root);assert.equal(result.modules,2);const map=JSON.parse(fs.readFileSync(path.join(preview,"import-map.json"),"utf8"));assert.match(map.imports[ENTRY],/^\.\/chunks\/playcraft-bundle\.js\?h=[a-f0-9]+$/);assert.equal(result.bundle,map.imports[ENTRY]);const bundle=fs.readFileSync(path.join(preview,"chunks/playcraft-bundle.js"),"utf8");assert.equal(bundle.includes("sourceMappingURL"),false);assert.equal(bundle.includes(`System.register("${ENTRY}"`),false);assert.match(bundle,/System\.register\("pack-chunk:bbb222"/);assert.match(bundle,/System\.register\(\["pack-chunk:bbb222","cc","pack-chunk:bbb222"\], function \(_export/);assert.equal(bundle.includes("\"@playcraft/adsdk/cjs\""),false);const last=bundle.trim().split("System.register").pop();assert.match(last,/^\(\[/);assert.equal(bundle.includes("functionfunction"),false);assert.equal(bundle.includes("cce:/internal/x/cc"),false)});it("is idempotent using the quickpack backup",()=>{const again=concatPreviewScripts(root);assert.equal(again.modules,2)})});
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{concatPreviewScripts,ENTRY}=require("./preview-concat.js");describe("concatPreviewScripts",()=>{let root;let preview;before(()=>{root=fs.mkdtempSync(path.join(os.tmpdir(),"preview-concat-"));preview=path.join(root,"temp/programming/packer-driver/targets/preview");fs.mkdirSync(path.join(preview,"chunks/aa"),{recursive:true});fs.mkdirSync(path.join(preview,"chunks/bb"),{recursive:true});fs.writeFileSync(path.join(preview,"chunks/aa/aaa111.js"),"System.register([\"__unresolved_0\", \"__unresolved_9\", \"cc\", \"@playcraft/adsdk/cjs\"], function (_export, _context) {\n return { setters: [], execute() {} };\n});\n//# sourceMappingURL=aaa111.js.map\n");fs.writeFileSync(path.join(preview,"chunks/bb/bbb222.js"),"System.register([], function (_export, _context) {\n return { setters: [], execute() {} };\n});\n");fs.writeFileSync(path.join(preview,"import-map.json"),JSON.stringify({imports:{[ENTRY]:"./chunks/aa/aaa111.js","cce:/internal/x/cc":"./chunks/cc/skip-me.js"},scopes:{"./chunks/aa/aaa111.js":{__unresolved_0:"./chunks/bb/bbb222.js","@playcraft/adsdk/cjs":"./chunks/bb/bbb222.js",cc:"./chunks/cc/skip-me.js"}}}))});after(()=>{fs.rmSync(root,{recursive:true,force:true})});it("writes one named-register bundle and a single import-map entry",()=>{const result=concatPreviewScripts(root);assert.equal(result.modules,2);const map=JSON.parse(fs.readFileSync(path.join(preview,"import-map.json"),"utf8"));assert.match(map.imports[ENTRY],/^\.\/chunks\/playcraft-bundle\.js\?h=[a-f0-9]+$/);assert.equal(result.bundle,map.imports[ENTRY]);const bundle=fs.readFileSync(path.join(preview,"chunks/playcraft-bundle.js"),"utf8");assert.equal(bundle.includes("sourceMappingURL"),false);assert.equal(bundle.includes(`System.register("${ENTRY}"`),false);assert.match(bundle,/System\.register\("pack-chunk:empty"/);assert.match(bundle,/System\.register\("pack-chunk:bbb222"/);assert.match(bundle,/System\.register\(\["pack-chunk:bbb222","pack-chunk:empty","cc","pack-chunk:bbb222"\], function \(_export/);assert.equal(bundle.includes("\"__unresolved_9\""),false);assert.equal(bundle.includes("\"@playcraft/adsdk/cjs\""),false);const last=bundle.trim().split("System.register").pop();assert.match(last,/^\(\[/);assert.equal(bundle.includes("functionfunction"),false);assert.equal(bundle.includes("cce:/internal/x/cc"),false)});it("is idempotent using the quickpack backup",()=>{const again=concatPreviewScripts(root);assert.equal(again.modules,2)})});
@@ -1,3 +1,3 @@
1
- "use strict";const fs=require("fs");const path=require("path");const JSON5=require("json5");const{getBase64FromFile,getAllFilesFromDir,sniffAudioMime}=require("./fs");const{THEME_ASSET_EXT}=require("./constants");const{validateThemeData}=require("../../utils/validateThemeData");function detectCurrentTheme(projectRoot,themeConfig){const entryFile=themeConfig&&themeConfig.entryFile||"src/theme/index.ts";const entryPath=path.join(projectRoot,entryFile);if(!fs.existsSync(entryPath))return null;const content=fs.readFileSync(entryPath,"utf8");const match=content.match(/from\s+['"]\.\/([^'"]+)['"]/);return match?match[1]:null}function isInlinableThemeAsset(filePath){if(THEME_ASSET_EXT.test(filePath))return true;try{return Boolean(sniffAudioMime(fs.readFileSync(filePath)))}catch{return false}}function collectDirAssets(assetsDir,keyPrefix){const map={};if(!fs.existsSync(assetsDir))return map;const root=assetsDir.replace(/\\/g,"/");for(const filePath of getAllFilesFromDir(assetsDir)){if(!isInlinableThemeAsset(filePath))continue;const rel=filePath.replace(/\\/g,"/").slice(root.length).replace(/^\//,"");map[`${keyPrefix}${rel}`]=getBase64FromFile(filePath)}return map}function collectReferencedFiles(projectRoot,sourceDir,themeName,mergedData,assetRecord){if(!mergedData||typeof mergedData!=="object")return;const themeRoot=path.join(projectRoot,sourceDir,themeName);for(const value of Object.values(mergedData)){if(typeof value==="string"){if(assetRecord[value])continue;const rel=value.replace(/^\.\//,"");const candidates=[path.join(themeRoot,rel),path.join(themeRoot,"assets",path.basename(rel)),path.resolve(projectRoot,rel)];const abs=candidates.find(p=>fs.existsSync(p)&&fs.statSync(p).isFile()&&isInlinableThemeAsset(p));if(abs)assetRecord[value]=getBase64FromFile(abs)}else if(value&&typeof value==="object"&&!Array.isArray(value)){collectReferencedFiles(projectRoot,sourceDir,themeName,value,assetRecord)}}}function loadThemeI18n(projectRoot,sourceDir,themeName){const p=path.join(projectRoot,sourceDir,themeName,"theme.i18n.runtime.json5");if(!fs.existsSync(p))return{};try{return JSON5.parse(fs.readFileSync(p,"utf8"))||{}}catch{return{}}}/**
1
+ "use strict";const fs=require("fs");const path=require("path");const JSON5=require("json5");const{getBase64FromFile,getAllFilesFromDir,sniffAudioMime}=require("./fs");const{THEME_ASSET_EXT}=require("./constants");const{validateThemeData}=require("../../utils/validateThemeData");const{loadThemeLevels,writeGeneratedLevels}=require("./theme-levels.js");function detectCurrentTheme(projectRoot,themeConfig){const entryFile=themeConfig&&themeConfig.entryFile||"src/theme/index.ts";const entryPath=path.join(projectRoot,entryFile);if(!fs.existsSync(entryPath))return null;const content=fs.readFileSync(entryPath,"utf8");const match=content.match(/from\s+['"]\.\/([^'"]+)['"]/);return match?match[1]:null}function isInlinableThemeAsset(filePath){if(THEME_ASSET_EXT.test(filePath))return true;try{return Boolean(sniffAudioMime(fs.readFileSync(filePath)))}catch{return false}}function collectDirAssets(assetsDir,keyPrefix){const map={};if(!fs.existsSync(assetsDir))return map;const root=assetsDir.replace(/\\/g,"/");for(const filePath of getAllFilesFromDir(assetsDir)){if(!isInlinableThemeAsset(filePath))continue;const rel=filePath.replace(/\\/g,"/").slice(root.length).replace(/^\//,"");map[`${keyPrefix}${rel}`]=getBase64FromFile(filePath)}return map}function collectReferencedFiles(projectRoot,sourceDir,themeName,mergedData,assetRecord){if(!mergedData||typeof mergedData!=="object")return;const themeRoot=path.join(projectRoot,sourceDir,themeName);for(const value of Object.values(mergedData)){if(typeof value==="string"){if(assetRecord[value])continue;const rel=value.replace(/^\.\//,"");const candidates=[path.join(themeRoot,rel),path.join(themeRoot,"assets",path.basename(rel)),path.resolve(projectRoot,rel)];const abs=candidates.find(p=>fs.existsSync(p)&&fs.statSync(p).isFile()&&isInlinableThemeAsset(p));if(abs)assetRecord[value]=getBase64FromFile(abs)}else if(value&&typeof value==="object"&&!Array.isArray(value)){collectReferencedFiles(projectRoot,sourceDir,themeName,value,assetRecord)}}}function loadThemeI18n(projectRoot,sourceDir,themeName){const p=path.join(projectRoot,sourceDir,themeName,"theme.i18n.runtime.json5");if(!fs.existsSync(p))return{};try{return JSON5.parse(fs.readFileSync(p,"utf8"))||{}}catch{return{}}}/**
2
2
  * Read src/theme json5 + theme/assets the same way Phaser webpack inlines them.
3
- */function loadThemePayload(projectRoot,themeName,themeConfig={}){const sourceDir=themeConfig.sourceDir||"src/theme";const{mergedData,errors}=validateThemeData(themeName,projectRoot,themeConfig);if(errors){throw new Error(`Theme "${themeName}" validation failed:\n${errors.join("\n")}`)}const assetsDir=path.join(projectRoot,sourceDir,themeName,"assets");const assetRecordByPath=collectDirAssets(assetsDir,"./assets/");collectReferencedFiles(projectRoot,sourceDir,themeName,mergedData,assetRecordByPath);return{themeName,mergedData:mergedData||{},i18nOverride:loadThemeI18n(projectRoot,sourceDir,themeName),assetRecordByPath}}function copyThemeAssetsToResources(projectRoot,themeName){const srcDir=path.join(projectRoot,"src/theme",themeName,"assets");const destDir=path.join(projectRoot,"assets/resources/textures");if(!fs.existsSync(srcDir)||!fs.existsSync(destDir))return false;let changed=false;for(const name of fs.readdirSync(srcDir)){if(!/\.(png|webp|jpe?g)$/i.test(name))continue;const dest=path.join(destDir,name);if(!fs.existsSync(dest))continue;const from=path.join(srcDir,name);const next=fs.readFileSync(from);const prev=fs.readFileSync(dest);if(Buffer.compare(next,prev)===0)continue;fs.writeFileSync(dest,next);changed=true}return changed}function themeAssetStamp(dir){if(!fs.existsSync(dir))return"";try{return fs.readdirSync(dir).filter(name=>THEME_ASSET_EXT.test(name)).map(name=>{const st=fs.statSync(path.join(dir,name));return`${name}:${st.size}:${Math.round(st.mtimeMs)}`}).join("|")}catch{return""}}function writeGeneratedThemeModule(filePath,payload){const header="/* eslint-disable */\n"+"// Generated from src/theme by playable-scripts. Do not edit.\n";const body=`export const THEME_DATA = ${JSON.stringify(payload.mergedData,null,2)} as Record<string, unknown>;\n`+`export const THEME_I18N_OVERRIDE = ${JSON.stringify(payload.i18nOverride,null,2)} as Record<string, Record<string, string>>;\n`+`export const THEME_ASSETS = ${JSON.stringify(payload.assetRecordByPath)} as Record<string, string>;\n`;const next=header+body;if(fs.existsSync(filePath)&&fs.readFileSync(filePath,"utf8")===next){return false}fs.mkdirSync(path.dirname(filePath),{recursive:true});fs.writeFileSync(filePath,next);return true}function syncGeneratedTheme(projectRoot,options={}){const themeConfig=options.themeConfig||{sourceDir:"src/theme",entryFile:"src/theme/index.ts"};const themeName=options.themeName||detectCurrentTheme(projectRoot,themeConfig);if(!themeName)return null;const payload=loadThemePayload(projectRoot,themeName,themeConfig);const rel=options.generatedRel||"assets/scripts/platform/theme.generated.ts";const copied=copyThemeAssetsToResources(projectRoot,themeName);const changed=writeGeneratedThemeModule(path.join(projectRoot,rel),payload);return{themeName,payload,generatedRel:rel,changed:changed||copied}}module.exports={detectCurrentTheme,loadThemePayload,writeGeneratedThemeModule,syncGeneratedTheme,collectDirAssets,themeAssetStamp};
3
+ */function loadThemePayload(projectRoot,themeName,themeConfig={}){const sourceDir=themeConfig.sourceDir||"src/theme";const{mergedData,errors}=validateThemeData(themeName,projectRoot,themeConfig);if(errors){throw new Error(`Theme "${themeName}" validation failed:\n${errors.join("\n")}`)}const assetsDir=path.join(projectRoot,sourceDir,themeName,"assets");const assetRecordByPath=collectDirAssets(assetsDir,"./assets/");collectReferencedFiles(projectRoot,sourceDir,themeName,mergedData,assetRecordByPath);return{themeName,mergedData:mergedData||{},i18nOverride:loadThemeI18n(projectRoot,sourceDir,themeName),assetRecordByPath,levels:loadThemeLevels(projectRoot,themeName,themeConfig)}}function copyThemeAssetsToResources(projectRoot,themeName){const srcDir=path.join(projectRoot,"src/theme",themeName,"assets");const destDir=path.join(projectRoot,"assets/resources/textures");if(!fs.existsSync(srcDir)||!fs.existsSync(destDir))return false;let changed=false;for(const name of fs.readdirSync(srcDir)){if(!/\.(png|webp|jpe?g)$/i.test(name))continue;const dest=path.join(destDir,name);if(!fs.existsSync(dest))continue;const from=path.join(srcDir,name);const next=fs.readFileSync(from);const prev=fs.readFileSync(dest);if(Buffer.compare(next,prev)===0)continue;fs.writeFileSync(dest,next);changed=true}return changed}function themeAssetStamp(dir){if(!fs.existsSync(dir))return"";try{return fs.readdirSync(dir).filter(name=>THEME_ASSET_EXT.test(name)).map(name=>{const st=fs.statSync(path.join(dir,name));return`${name}:${st.size}:${Math.round(st.mtimeMs)}`}).join("|")}catch{return""}}function writeGeneratedThemeModule(filePath,payload){const header="/* eslint-disable */\n"+"// Generated from src/theme by playable-scripts. Do not edit.\n";const body=`export const THEME_DATA = ${JSON.stringify(payload.mergedData,null,2)} as Record<string, unknown>;\n`+`export const THEME_I18N_OVERRIDE = ${JSON.stringify(payload.i18nOverride,null,2)} as Record<string, Record<string, string>>;\n`+`export const THEME_ASSETS = ${JSON.stringify(payload.assetRecordByPath)} as Record<string, string>;\n`;const next=header+body;if(fs.existsSync(filePath)&&fs.readFileSync(filePath,"utf8")===next){return false}fs.mkdirSync(path.dirname(filePath),{recursive:true});fs.writeFileSync(filePath,next);return true}function syncGeneratedTheme(projectRoot,options={}){const themeConfig=options.themeConfig||{sourceDir:"src/theme",entryFile:"src/theme/index.ts"};const themeName=options.themeName||detectCurrentTheme(projectRoot,themeConfig);if(!themeName)return null;const payload=loadThemePayload(projectRoot,themeName,themeConfig);const rel=options.generatedRel||"assets/scripts/platform/theme.generated.ts";const copied=copyThemeAssetsToResources(projectRoot,themeName);const changed=writeGeneratedThemeModule(path.join(projectRoot,rel),payload);const levelsRel=options.levelsGeneratedRel||"assets/scripts/data/levels.generated.ts";const levelsChanged=writeGeneratedLevels(path.join(projectRoot,levelsRel),payload.levels||[]);return{themeName,payload,generatedRel:rel,changed:changed||copied||levelsChanged}}module.exports={detectCurrentTheme,loadThemePayload,writeGeneratedThemeModule,syncGeneratedTheme,collectDirAssets,themeAssetStamp};
@@ -0,0 +1 @@
1
+ "use strict";const fs=require("fs");const path=require("path");function loadThemeLevels(projectRoot,themeName,themeConfig={}){const sourceDir=themeConfig.sourceDir||"src/theme";const dir=path.join(projectRoot,sourceDir,themeName,"levels");if(!fs.existsSync(dir))return[];let ids=[];const indexPath=path.join(dir,"index.json");if(fs.existsSync(indexPath)){const index=JSON.parse(fs.readFileSync(indexPath,"utf8"));ids=Array.isArray(index.levels)?index.levels.map(String):[]}else{ids=fs.readdirSync(dir).filter(name=>name.endsWith(".json")&&name!=="index.json"&&name!=="palette.json").map(name=>name.replace(/\.json$/i,"")).sort()}return ids.map(id=>{const filePath=path.join(dir,`${id}.json`);if(!fs.existsSync(filePath)){throw new Error(`[levels] missing ${path.relative(projectRoot,filePath)}`)}const raw=JSON.parse(fs.readFileSync(filePath,"utf8"));if(!raw||typeof raw!=="object"||Array.isArray(raw)){throw new Error(`[levels] ${id}.json must be an object`)}return{...raw,id:raw.id||id}})}function writeGeneratedLevels(filePath,levels){const header="/* eslint-disable */\n"+"// Generated from src/theme/<theme>/levels. Do not edit.\n";const body=`export const THEME_LEVELS = ${JSON.stringify(levels,null,2)} as Record<string, unknown>[];\n`;const next=header+body;if(fs.existsSync(filePath)&&fs.readFileSync(filePath,"utf8")===next){return false}fs.mkdirSync(path.dirname(filePath),{recursive:true});fs.writeFileSync(filePath,next);return true}function detectThemeName(projectRoot,themeConfig={}){const entryFile=themeConfig.entryFile||"src/theme/index.ts";const entryPath=path.join(projectRoot,entryFile);if(!fs.existsSync(entryPath))return null;const match=fs.readFileSync(entryPath,"utf8").match(/from\s+['"]\.\/([^'"]+)['"]/);return match?match[1]:null}function syncGeneratedLevels(projectRoot,options={}){const themeConfig=options.themeConfig||{sourceDir:"src/theme",entryFile:"src/theme/index.ts"};const themeName=options.themeName||detectThemeName(projectRoot,themeConfig);if(!themeName)return null;const levels=loadThemeLevels(projectRoot,themeName,themeConfig);const rel=options.generatedRel||"assets/scripts/data/levels.generated.ts";const changed=writeGeneratedLevels(path.join(projectRoot,rel),levels);return{themeName,levels,generatedRel:rel,changed}}module.exports={loadThemeLevels,writeGeneratedLevels,syncGeneratedLevels};
@@ -4,6 +4,7 @@
4
4
  var AK_CLICK_DESTINATION_URL = "PLACEHOLDER_CLICK_REDIRECT";
5
5
  var AK_CLICK_PIXEL_URL = "PLACEHOLDER_CLICK_PIXEL";
6
6
  </script>
7
+ <div></div>
7
8
  <script type="application/javascript" src="creative.js"></script>`;const BIGABID_INDEX=`<meta charset="UTF-8">
8
9
  <script src="mraid.js"></script>
9
10
  <script>window.BIGABID_BIDTIMEMACROS = {
@@ -21,7 +21,11 @@
21
21
  * - 仅在打正式包(生产环境)时使用
22
22
  * - 压缩等级固定为 9(最高压缩)
23
23
  * - 不保留原始文件
24
- */const fflate=require("fflate");const fs=require("fs");const path=require("path");const{minify}=require("terser");class FflateCompressionPlugin{/**
24
+ */const fflate=require("fflate");const fs=require("fs");const path=require("path");const{minify}=require("terser");/**
25
+ * Collapse HTML whitespace without touching script bodies.
26
+ * Stripping newlines inside a script turns `// comment` into a comment that
27
+ * eats the rest of that script (and, after Adikteev concat, the next files).
28
+ */function minifyHtmlKeepingScripts(html){const scripts=[];const without=String(html).replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi,block=>{scripts.push(block);return`%%PLAYABLE_SCRIPT_${scripts.length-1}%%`});const minified=without.replace(/\n\s*/g,"").replace(/>\s+</g,"><").replace(/\s{2,}/g," ").trim();return minified.replace(/%%PLAYABLE_SCRIPT_(\d+)%%/g,(_,i)=>scripts[Number(i)])}class FflateCompressionPlugin{/**
25
29
  * @param {Object} options - 插件配置选项
26
30
  * @param {boolean} [options.enabled=true] - 是否启用压缩
27
31
  * @param {number} [options.level=9] - 压缩等级(固定为 9)
@@ -71,76 +75,126 @@ const hasHeadContent=headContent.length>0;const decompressCodeReadable=`
71
75
  (function() {
72
76
  var compressedData = '${compressedBase64}';
73
77
  var __hasHeadContent = ${hasHeadContent};
74
-
75
- try {
76
- // Base64 解码为 Uint8Array
77
- var compressedArray = Uint8Array.from(atob(compressedData), function(c) {
78
- return c.charCodeAt(0);
79
- });
80
-
81
- // 使用 fflate 解压
82
- var decompressedArray = fflate.unzlibSync(compressedArray);
83
- var htmlString = new TextDecoder().decode(decompressedArray);
84
-
85
- // 解析 HTML
86
- var parser = new DOMParser();
87
- var doc = parser.parseFromString(htmlString, 'text/html');
88
-
89
- // 替换 body
90
- document.body.replaceWith(doc.body);
91
-
92
- // 恢复 head 中的资源引用和内联脚本
93
- // 如果原始 HTML 有 <head>(__hasHeadContent=true),外部资源已由 extractHeadContent
94
- // 提取并放在模板 head 中,这里只需追加未被模板覆盖的内联脚本和样式
95
- // 如果原始 HTML 是片段(__hasHeadContent=false),如 Adikteev 格式,
96
- // 所有资源引用都在压缩数据中,需要完整恢复——包括外部脚本和样式表
97
- //
98
- // 重要:动态注入的外部脚本在页面解析完成后才异步加载执行,
99
- // 此时 DOMContentLoaded 已触发,脚本内的事件监听器将永远无法执行。
100
- // 因此在外部脚本加载完成后手动 dispatch DOMContentLoaded,确保游戏等代码正常初始化。
101
- var __extScriptCount = 0;
102
- var __extScriptLoaded = 0;
103
- Array.from(doc.head.children).forEach(function(child) {
104
- if (child.tagName === 'SCRIPT') {
105
- if (!child.src && child.textContent) {
106
- // 内联 script(有内容但无 src)——始终需要恢复
107
- var script = document.createElement('script');
108
- script.textContent = child.textContent;
109
- if (child.type) script.type = child.type;
110
- document.head.appendChild(script);
111
- } else if (child.src && !__hasHeadContent) {
112
- // 外部 script(有 src 属性,如 <script src="creative.js">)
78
+
79
+ function ensureBody() {
80
+ if (document.body) return document.body;
81
+ var root = document.documentElement;
82
+ if (!root) {
83
+ root = document.createElement('html');
84
+ document.appendChild(root);
85
+ }
86
+ var body = document.createElement('body');
87
+ root.appendChild(body);
88
+ return body;
89
+ }
90
+
91
+ function showDecompressError(error) {
92
+ console.error('Decompress failed:', error);
93
+ try {
94
+ ensureBody().innerHTML = '<div style="padding:20px;color:#fff;background:#000">' +
95
+ '<h2>Error</h2><p>' + error.message + '</p></div>';
96
+ } catch (e) {}
97
+ }
98
+
99
+ function apply() {
100
+ try {
101
+ // Base64 解码为 Uint8Array
102
+ var compressedArray = Uint8Array.from(atob(compressedData), function(c) {
103
+ return c.charCodeAt(0);
104
+ });
105
+
106
+ // 使用 fflate 解压
107
+ var decompressedArray = fflate.unzlibSync(compressedArray);
108
+ var htmlString = new TextDecoder().decode(decompressedArray);
109
+
110
+ // 解析 HTML
111
+ var parser = new DOMParser();
112
+ var doc = parser.parseFromString(htmlString, 'text/html');
113
+ if (!doc.body) {
114
+ throw new Error('Decompressed HTML has no body');
115
+ }
116
+
117
+ // Adikteev / Bigabid 会把解压脚本抽到外部 JS。渠道 HTML 是片段,
118
+ // 脚本常在隐式 head 里同步执行,此时 document.body 还是 null。
119
+ ensureBody().replaceWith(doc.body);
120
+
121
+ // 恢复 head 中的资源引用和内联脚本
122
+ // 如果原始 HTML 有 <head>(__hasHeadContent=true),外部资源已由 extractHeadContent
123
+ // 提取并放在模板 head 中,这里只需追加未被模板覆盖的内联脚本和样式
124
+ // 如果原始 HTML 是片段(__hasHeadContent=false),如 Adikteev 格式,
125
+ // 所有资源引用都在压缩数据中,需要完整恢复——包括外部脚本和样式表
126
+ //
127
+ // 重要:动态注入的外部脚本在页面解析完成后才异步加载执行,
128
+ // 此时 DOMContentLoaded 已触发,脚本内的事件监听器将永远无法执行。
129
+ // 因此在外部脚本加载完成后手动 dispatch DOMContentLoaded,确保游戏等代码正常初始化。
130
+ var __extScriptCount = 0;
131
+ var __extScriptLoaded = 0;
132
+ var headChildren = doc.head ? Array.from(doc.head.children) : [];
133
+ headChildren.forEach(function(child) {
134
+ if (child.tagName === 'SCRIPT') {
135
+ if (!child.src && child.textContent) {
136
+ // 内联 script(有内容但无 src)——始终需要恢复
137
+ var script = document.createElement('script');
138
+ script.textContent = child.textContent;
139
+ if (child.type) script.type = child.type;
140
+ (document.head || document.documentElement).appendChild(script);
141
+ } else if (child.src && !__hasHeadContent) {
142
+ // 外部 script(有 src 属性,如 <script src="creative.js">)
143
+ // 仅在原始 HTML 无 <head> 时恢复,避免与模板中已提取的重复
144
+ __extScriptCount++;
145
+ var extScript = document.createElement('script');
146
+ extScript.src = child.src;
147
+ if (child.type) extScript.type = child.type;
148
+ if (child.async) extScript.async = true;
149
+ if (child.defer) extScript.defer = true;
150
+ extScript.onload = function() {
151
+ __extScriptLoaded++;
152
+ if (__extScriptLoaded >= __extScriptCount) {
153
+ document.dispatchEvent(new Event('DOMContentLoaded'));
154
+ }
155
+ };
156
+ (document.head || document.documentElement).appendChild(extScript);
157
+ }
158
+ } else if (child.tagName === 'STYLE') {
159
+ // 内联 style——始终需要恢复
160
+ (document.head || document.documentElement).appendChild(child.cloneNode(true));
161
+ } else if (child.tagName === 'LINK' && /stylesheet/i.test(child.rel) && !__hasHeadContent) {
162
+ // 外部样式表(如 <link rel="stylesheet" href="style.css">)
113
163
  // 仅在原始 HTML 无 <head> 时恢复,避免与模板中已提取的重复
114
- __extScriptCount++;
115
- var extScript = document.createElement('script');
116
- extScript.src = child.src;
117
- if (child.type) extScript.type = child.type;
118
- if (child.async) extScript.async = true;
119
- if (child.defer) extScript.defer = true;
120
- extScript.onload = function() {
121
- __extScriptLoaded++;
122
- if (__extScriptLoaded >= __extScriptCount) {
123
- document.dispatchEvent(new Event('DOMContentLoaded'));
124
- }
125
- };
126
- document.head.appendChild(extScript);
164
+ var link = document.createElement('link');
165
+ link.rel = 'stylesheet';
166
+ link.href = child.href;
167
+ (document.head || document.documentElement).appendChild(link);
127
168
  }
128
- } else if (child.tagName === 'STYLE') {
129
- // 内联 style——始终需要恢复
130
- document.head.appendChild(child.cloneNode(true));
131
- } else if (child.tagName === 'LINK' && /stylesheet/i.test(child.rel) && !__hasHeadContent) {
132
- // 外部样式表(如 <link rel="stylesheet" href="style.css">)
133
- // 仅在原始 HTML 无 <head> 时恢复,避免与模板中已提取的重复
134
- var link = document.createElement('link');
135
- link.rel = 'stylesheet';
136
- link.href = child.href;
137
- document.head.appendChild(link);
138
- }
139
- });
140
- } catch (error) {
141
- console.error('Decompress failed:', error);
142
- document.body.innerHTML = '<div style="padding:20px;color:#fff;background:#000">' +
143
- '<h2>Error</h2><p>' + error.message + '</p></div>';
169
+ });
170
+
171
+ // DOMParser 解析出的 script 被 replaceWith 进当前文档后不会执行。
172
+ // head 内联脚本上面已经重建;body 里的资源表和 adapter 也必须重建,
173
+ // 否则引擎脚本不跑,页面会停在空白画布。
174
+ Array.from(document.body.querySelectorAll('script')).forEach(function(oldScript) {
175
+ var script = document.createElement('script');
176
+ Array.from(oldScript.attributes).forEach(function(attr) {
177
+ script.setAttribute(attr.name, attr.value);
178
+ });
179
+ var src = oldScript.getAttribute('src');
180
+ if (!src) {
181
+ script.textContent = oldScript.textContent;
182
+ } else if (!oldScript.hasAttribute('async') && !oldScript.hasAttribute('defer')) {
183
+ script.async = false;
184
+ }
185
+ oldScript.replaceWith(script);
186
+ });
187
+ } catch (error) {
188
+ showDecompressError(error);
189
+ }
190
+ }
191
+
192
+ if (document.body) {
193
+ apply();
194
+ } else if (document.readyState === 'loading') {
195
+ document.addEventListener('DOMContentLoaded', apply);
196
+ } else {
197
+ apply();
144
198
  }
145
199
  })();
146
200
  `;// 使用 Terser 压缩解压代码
@@ -196,8 +250,8 @@ const htmlTemplate=`
196
250
  <script>${decompressCode}</script>
197
251
  <script>
198
252
  (function() {
199
- // 为了通过渠道的检测,伪造几个对象和方法 FbPlayableAd.onCTAClick(); ExitApi.exit();window.BGY_MRAID.open() mraid.open(destinationUrl); dapi.openStoreUrl(); NUC.trigger.convert(destinationUrl)
200
- // ScPlayableAd.onCTAClick(); smxTracking.redirect(); window.install();window.TJ_API.click(); window.openAppStore(); window.open(destinationUrl);
253
+ /* 渠道静态扫描:FbPlayableAd.onCTAClick(); ExitApi.exit();window.BGY_MRAID.open() mraid.open(destinationUrl); dapi.openStoreUrl(); NUC.trigger.convert(destinationUrl) */
254
+ /* ScPlayableAd.onCTAClick(); smxTracking.redirect(); window.install();window.TJ_API.click(); window.openAppStore(); window.open(destinationUrl); */
201
255
  var FbPlayableAd = {
202
256
  onCTAClick: function() {}
203
257
  };
@@ -207,7 +261,6 @@ const htmlTemplate=`
207
261
  };
208
262
  ExitApi.exit();
209
263
 
210
- // 根据 UA 判断商店链接
211
264
  var GOOGLE_PLAY_URL = '${this.options.googlePlayUrl}';
212
265
  var APP_STORE_URL = '${this.options.appStoreUrl}';
213
266
  var isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
@@ -222,14 +275,12 @@ const htmlTemplate=`
222
275
  * 实际的 MRAID 调用在解压后的业务代码中执行
223
276
  */
224
277
  if (false) {
225
- // MRAID 事件监听
278
+ /* MRAID 事件监听 */
226
279
  mraid.addEventListener("ready", function() {});
227
280
  mraid.addEventListener("viewableChange", function(isViewable) {
228
281
  if (isViewable) {
229
- // 广告可见,开始播放游戏和音频
230
282
  game.sound.resumeAll();
231
283
  } else {
232
- // 广告不可见,暂停游戏和音频
233
284
  game.sound.pauseAll();
234
285
  }
235
286
  });
@@ -238,14 +289,11 @@ const htmlTemplate=`
238
289
  mraid.addEventListener("orientationChange", function() {});
239
290
  mraid.addEventListener("resize", function() {});
240
291
 
241
- // 音频控制
242
292
  var audio = new Audio();
243
293
  audio.play();
244
294
  audio.pause();
245
295
  audio.muted = true;
246
296
  audio.volume = 0;
247
-
248
- // 商店跳转
249
297
  var isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
250
298
  var destinationUrl = isIOS
251
299
  ? '${this.options.appStoreUrl}'
@@ -255,8 +303,4 @@ const htmlTemplate=`
255
303
  </script>
256
304
  </body>
257
305
  </html>
258
- `;// 手动压缩 HTML(去除多余空格和换行,但保留必要的空格)
259
- const minifiedHtml=htmlTemplate.replace(/\n\s*/g,"")// 去除换行和缩进
260
- .replace(/>\s+</g,"><")// 去除标签间的空格
261
- .replace(/\s{2,}/g," ")// 多个空格压缩为一个
262
- .trim();return minifiedHtml}}module.exports={FflateCompressionPlugin};
306
+ `;return minifyHtmlKeepingScripts(htmlTemplate)}}module.exports={FflateCompressionPlugin,minifyHtmlKeepingScripts};
@@ -0,0 +1,13 @@
1
+ "use strict";const{describe,it}=require("node:test");const assert=require("node:assert/strict");const{minifyHtmlKeepingScripts}=require("./FflateCompressionPlugin.js");const{extractInlineScripts}=require("../injectors/apply.js");describe("minifyHtmlKeepingScripts",()=>{it("does not let // comments eat the rest of a script after newline collapse",()=>{const html=`
2
+ <!DOCTYPE html><html><body>
3
+ <script>
4
+ (function() {
5
+ // decoy scanner tokens mraid.open(destinationUrl)
6
+ var ExitApi = { exit: function() {} };
7
+ ExitApi.exit();
8
+ }());
9
+ </script>
10
+ <script>
11
+ if (false) { mraid.open("x"); }
12
+ </script>
13
+ </body></html>`;const minified=minifyHtmlKeepingScripts(html);const{scripts}=extractInlineScripts(minified);const joined=scripts.join("\n");assert.match(joined,/ExitApi\.exit\(\)/);assert.match(joined,/mraid\.open\("x"\)/);assert.doesNotThrow(()=>new Function(joined))})});describe("fflate decompress runtime",()=>{it("waits for document.body before replaceWith (Adikteev fragment / head script)",async()=>{const{FflateCompressionPlugin}=require("./FflateCompressionPlugin.js");const plugin=new FflateCompressionPlugin;const html=await plugin.generateDecompressHtml("YQ==","var fflate={};","");assert.match(html,/DOMContentLoaded/);assert.match(html,/ensureBody|document\.body/);const{scripts}=extractInlineScripts(html);const joined=scripts.join("\n");assert.match(joined,/DOMContentLoaded/);assert.doesNotThrow(()=>new Function(joined))})});
@@ -15,6 +15,7 @@
15
15
  var AK_CLICK_DESTINATION_URL = "PLACEHOLDER_CLICK_REDIRECT";
16
16
  var AK_CLICK_PIXEL_URL = "PLACEHOLDER_CLICK_PIXEL";
17
17
  </script>
18
+ <div></div>
18
19
  <script type="application/javascript" src="creative.js"></script>`;/**
19
20
  * 生成 Adikteev 平台所需的 HTML 配置
20
21
  *
@@ -3,8 +3,12 @@
3
3
  * 0 none, 1 terser, 2 basic obfuscator, 3 + dead code, 4 maximum.
4
4
  * Cocos applies this only to business bundles (assets/main/index.js).
5
5
  * Order matches webpack: obfuscator loader, then Terser minimizer.
6
- */const LEVEL_NAMES=["none","terser-only","basic","dead-code","maximum"];function obfuscateLevelName(level){const n=Number(level)||0;return LEVEL_NAMES[n]||LEVEL_NAMES[0]}function obfuscatorOptionsForLevel(level){const n=Number(level)||0;if(n<2)return null;const options={target:"browser",sourceMap:false,selfDefending:false,renameProperties:false,disableConsoleOutput:true,splitStrings:false,unicodeEscapeSequence:false};if(n===2){Object.assign(options,{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(n===3){Object.assign(options,{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{Object.assign(options,{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})}return options}function terserOptions(){return{safari10:true,mangle:{reserved:["$"],toplevel:true},compress:{drop_console:true,arrows:false,drop_debugger:true,unused:true,dead_code:true,passes:3,inline:3,collapse_vars:true,reduce_vars:true,hoist_vars:true,hoist_funs:true,sequences:20,properties:true,conditionals:true,comparisons:true,booleans:true,loops:true,if_return:true,join_vars:true,negate_iife:true,side_effects:true,switches:true,typeofs:true,unsafe:false,keep_fargs:false},output:{comments:false,quote_style:3,beautify:false,wrap_iife:true,ecma:2015},toplevel:true,keep_classnames:false,keep_fnames:false}}/**
6
+ */const LEVEL_NAMES=["none","terser-only","basic","dead-code","maximum"];function obfuscateLevelName(level){const n=Number(level)||0;return LEVEL_NAMES[n]||LEVEL_NAMES[0]}function obfuscatorOptionsForLevel(level){const n=Number(level)||0;if(n<2)return null;const options={target:"browser",sourceMap:false,selfDefending:false,renameProperties:false,disableConsoleOutput:true,splitStrings:false,unicodeEscapeSequence:false};if(n===2){Object.assign(options,{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(n===3){Object.assign(options,{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{Object.assign(options,{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})}return options}/**
7
+ * Cocos obfuscates one already-minified main bundle, not per-source files.
8
+ * Phaser's level 4 (RC4 + control-flow 1 + splitStrings) does not finish on that file.
9
+ * Keep stringArrayThreshold at 1 so literals are covered, and drop the options that hang.
10
+ */function obfuscatorOptionsForCocos(level){const options=obfuscatorOptionsForLevel(level);if(!options||Number(level)<4)return options;return{...options,stringArrayEncoding:["base64"],stringArrayThreshold:1,controlFlowFlattening:true,controlFlowFlatteningThreshold:0.75,deadCodeInjection:true,deadCodeInjectionThreshold:0.4,stringArrayWrappersCount:2,stringArrayCallsTransformThreshold:0.75,splitStrings:false,unicodeEscapeSequence:false}}function terserOptions(){return{safari10:true,mangle:{reserved:["$"],toplevel:true},compress:{drop_console:true,arrows:false,drop_debugger:true,unused:true,dead_code:true,passes:3,inline:3,collapse_vars:true,reduce_vars:true,hoist_vars:true,hoist_funs:true,sequences:20,properties:true,conditionals:true,comparisons:true,booleans:true,loops:true,if_return:true,join_vars:true,negate_iife:true,side_effects:true,switches:true,typeofs:true,unsafe:false,keep_fargs:false},output:{comments:false,quote_style:3,beautify:false,wrap_iife:true,ecma:2015},toplevel:true,keep_classnames:false,keep_fnames:false}}/**
7
11
  * Creator web-mobile gameplay bundle. Engine (`cocos-js`, `assets/internal`)
8
12
  * and the resources asset table stay as Creator emitted them.
9
13
  * @param {string} objKey path inside the web-mobile folder
10
- */function isCocosBusinessScript(objKey){const key=String(objKey||"").replace(/\\/g,"/").replace(/^\//,"");return /^assets\/main\/index(?:\.[^/]+)?\.js$/.test(key)}async function applyObfuscation(code,level){const n=Number(level)||0;if(n<=0||typeof code!=="string"||!code)return code;let out=code;const obf=obfuscatorOptionsForLevel(n);if(obf){const JavaScriptObfuscator=require("javascript-obfuscator");out=JavaScriptObfuscator.obfuscate(out,obf).getObfuscatedCode()}if(n>=1){const{minify}=require("terser");const result=await minify(out,terserOptions());if(!result||!result.code){throw new Error(`terser produced empty output at obfuscate level ${n}`)}out=result.code}return out}module.exports={LEVEL_NAMES,obfuscateLevelName,obfuscatorOptionsForLevel,terserOptions,isCocosBusinessScript,applyObfuscation};
14
+ */function isCocosBusinessScript(objKey){const key=String(objKey||"").replace(/\\/g,"/").replace(/^\//,"");return /^assets\/main\/index(?:\.[^/]+)?\.js$/.test(key)}async function applyObfuscation(code,level){const n=Number(level)||0;if(n<=0||typeof code!=="string"||!code)return code;let out=code;const obf=obfuscatorOptionsForCocos(n);if(obf){if(n>=4){console.log("[cocos] level 4 on the main bundle uses base64 string arrays; RC4 and full control-flow do not finish")}const JavaScriptObfuscator=require("javascript-obfuscator");out=JavaScriptObfuscator.obfuscate(out,obf).getObfuscatedCode()}if(n>=1){const{minify}=require("terser");const result=await minify(out,terserOptions());if(!result||!result.code){throw new Error(`terser produced empty output at obfuscate level ${n}`)}out=result.code}return out}module.exports={LEVEL_NAMES,obfuscateLevelName,obfuscatorOptionsForLevel,obfuscatorOptionsForCocos,terserOptions,isCocosBusinessScript,applyObfuscation};
@@ -1,4 +1,4 @@
1
- "use strict";const{describe,it}=require("node:test");const assert=require("node:assert/strict");const vm=require("vm");const{isCocosBusinessScript,applyObfuscation,obfuscatorOptionsForLevel}=require("./obfuscateLevels.js");const SAMPLE=`
1
+ "use strict";const{describe,it}=require("node:test");const assert=require("node:assert/strict");const vm=require("vm");const{isCocosBusinessScript,applyObfuscation,obfuscatorOptionsForLevel,obfuscatorOptionsForCocos}=require("./obfuscateLevels.js");const SAMPLE=`
2
2
  System.register("chunks:///_virtual/Demo.js", ["./rollupPluginModLoBabelHelpers.js", "cc"], function (exports) {
3
3
  return {
4
4
  setters: [function () {}, function () {}],
@@ -7,4 +7,4 @@ System.register("chunks:///_virtual/Demo.js", ["./rollupPluginModLoBabelHelpers.
7
7
  }
8
8
  };
9
9
  });
10
- `;function loadRegisters(code){const registered=[];const context=vm.createContext({System:{register(name,deps,factory){const ns=factory(function(){});registered.push({name:String(name),deps:Array.from(deps,dep=>String(dep)),keys:ns&&typeof ns==="object"?Object.keys(ns).map(String).sort():[]})}},console});vm.runInContext(code,context,{timeout:5000});return registered}describe("isCocosBusinessScript",()=>{it("matches the main bundle only",()=>{assert.equal(isCocosBusinessScript("assets/main/index.js"),true);assert.equal(isCocosBusinessScript("assets/main/index.abc123.js"),true);assert.equal(isCocosBusinessScript("assets/internal/index.js"),false);assert.equal(isCocosBusinessScript("assets/resources/index.js"),false);assert.equal(isCocosBusinessScript("cocos-js/cc.js"),false)})});describe("applyObfuscation",()=>{it("keeps System.register setters and execute at every level",async()=>{const original=loadRegisters(SAMPLE);assert.deepEqual(original[0].keys,["execute","setters"]);for(const level of[1,2,3,4]){const out=await applyObfuscation(SAMPLE,level);assert.equal(typeof out,"string");assert.ok(out.length>0,`level ${level} empty`);const registered=loadRegisters(out);assert.equal(registered.length,1,`level ${level} register count`);assert.deepEqual(registered[0].keys,["execute","setters"],`level ${level} keys`);assert.deepEqual(registered[0].deps,original[0].deps,`level ${level} deps`)}});it("level 0 and 1 do not enable the obfuscator",()=>{assert.equal(obfuscatorOptionsForLevel(0),null);assert.equal(obfuscatorOptionsForLevel(1),null);assert.equal(obfuscatorOptionsForLevel(2).stringArray,true);assert.equal(obfuscatorOptionsForLevel(3).deadCodeInjection,true);assert.equal(obfuscatorOptionsForLevel(4).stringArrayEncoding[0],"rc4")})});
10
+ `;function loadRegisters(code){const registered=[];const context=vm.createContext({System:{register(name,deps,factory){const ns=factory(function(){});registered.push({name:String(name),deps:Array.from(deps,dep=>String(dep)),keys:ns&&typeof ns==="object"?Object.keys(ns).map(String).sort():[]})}},console});vm.runInContext(code,context,{timeout:5000});return registered}describe("isCocosBusinessScript",()=>{it("matches the main bundle only",()=>{assert.equal(isCocosBusinessScript("assets/main/index.js"),true);assert.equal(isCocosBusinessScript("assets/main/index.abc123.js"),true);assert.equal(isCocosBusinessScript("assets/internal/index.js"),false);assert.equal(isCocosBusinessScript("assets/resources/index.js"),false);assert.equal(isCocosBusinessScript("cocos-js/cc.js"),false)})});describe("applyObfuscation",()=>{it("keeps System.register setters and execute at every level",async()=>{const original=loadRegisters(SAMPLE);assert.deepEqual(original[0].keys,["execute","setters"]);for(const level of[1,2,3,4]){const out=await applyObfuscation(SAMPLE,level);assert.equal(typeof out,"string");assert.ok(out.length>0,`level ${level} empty`);const registered=loadRegisters(out);assert.equal(registered.length,1,`level ${level} register count`);assert.deepEqual(registered[0].keys,["execute","setters"],`level ${level} keys`);assert.deepEqual(registered[0].deps,original[0].deps,`level ${level} deps`)}});it("level 0 and 1 do not enable the obfuscator",()=>{assert.equal(obfuscatorOptionsForLevel(0),null);assert.equal(obfuscatorOptionsForLevel(1),null);assert.equal(obfuscatorOptionsForLevel(2).stringArray,true);assert.equal(obfuscatorOptionsForLevel(3).deadCodeInjection,true);assert.equal(obfuscatorOptionsForLevel(4).stringArrayEncoding[0],"rc4");assert.equal(obfuscatorOptionsForCocos(4).stringArrayEncoding[0],"base64");assert.equal(obfuscatorOptionsForCocos(4).stringArrayThreshold,1);assert.equal(obfuscatorOptionsForCocos(4).splitStrings,false)})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/devkit",
3
- "version": "1.0.31-beta.12",
3
+ "version": "1.0.31-beta.14",
4
4
  "description": "HTML5 Playable Ads 构建工具,支持多广告渠道打包",
5
5
  "main": "core/index.js",
6
6
  "module": "core/index.js",