@playcraft/devkit 1.0.31-beta.14 → 1.0.31-beta.16
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.
|
@@ -1 +1,5 @@
|
|
|
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)||"."}`)}
|
|
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)||"."}`)}function generatedThemeRel(buildOptions){return buildOptions.cocos&&buildOptions.cocos.generatedThemeFile||"assets/scripts/platform/theme.generated.ts"}/**
|
|
2
|
+
* Channel emit already inlines window.__PLAYCRAFT_THEME_ASSETS__.
|
|
3
|
+
* If those data URLs also live in theme.generated.ts, Cocos bakes a second
|
|
4
|
+
* copy into main/index.js and the HTML grows by the whole theme (~3MB+).
|
|
5
|
+
*/function stripThemeAssetsForEngineCompile(root,themeName,themeConfig,generatedRel){let payload;try{payload=loadThemePayload(root,themeName,themeConfig)}catch(err){console.warn(`[cocos] theme compile strip skipped: ${err.message}`);return null}writeGeneratedThemeModule(path.join(root,generatedRel),{...payload,assetRecordByPath:{}});const bytes=Object.values(payload.assetRecordByPath||{}).reduce((n,v)=>n+String(v).length,0);console.log(`[cocos] omit ${Object.keys(payload.assetRecordByPath||{}).length} THEME_ASSETS from engine compile (${(bytes/1024).toFixed(0)}kb, inject at emit)`);return payload}function restoreGeneratedTheme(root,generatedRel,payload){if(!payload)return;writeGeneratedThemeModule(path.join(root,generatedRel),payload)}async function prepareCocosTemplate(customOptions){const root=process.cwd();const buildOptions=withBuildsPackFlags(root,mergeOptions(options,customOptions));const themeConfig=loadBuildsThemeConfig(root);const themeName=resolveThemeName(root,buildOptions,themeConfig);const generated=generatedThemeRel(buildOptions);const compilePayload=stripThemeAssetsForEngineCompile(root,themeName,themeConfig,generated);try{await runCocosEngineBuild(root,buildOptions);await rebundlePreviewMerge(root);return await mergeTemplate(root,buildOptions)}finally{restoreGeneratedTheme(root,generated,compilePayload)}}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}const themeConfig=config.themes||loadBuildsThemeConfig(projectRoot);const compileTheme=themes[0]||detectCurrentTheme(projectRoot,themeConfig)||"default";const generated=generatedThemeRel(buildOptionsBase);const compilePayload=stripThemeAssetsForEngineCompile(projectRoot,compileTheme,themeConfig,generated);let templateHtml;try{await runCocosEngineBuild(projectRoot,buildOptionsBase);await rebundlePreviewMerge(projectRoot);templateHtml=await mergeTemplate(projectRoot,buildOptionsBase)}finally{restoreGeneratedTheme(projectRoot,generated,compilePayload)}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};
|
|
@@ -11,4 +11,4 @@
|
|
|
11
11
|
* Creator web-mobile gameplay bundle. Engine (`cocos-js`, `assets/internal`)
|
|
12
12
|
* and the resources asset table stay as Creator emitted them.
|
|
13
13
|
* @param {string} objKey path inside the web-mobile folder
|
|
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(
|
|
14
|
+
*/function isCocosBusinessScript(objKey){const key=String(objKey||"").replace(/\\/g,"/").replace(/^\//,"");return /^assets\/main\/index(?:\.[^/]+)?\.js$/.test(key)}/** Slot for asset payloads. Game logic still goes through the obfuscator. */const RAW_ASSET_SLOT="__PLAYCRAFT_RAW__";function decodeJsString(body){return body.replace(/\\(.)/g,(_,ch)=>{if(ch==="n")return"\n";if(ch==="r")return"\r";if(ch==="t")return"\t";return ch})}/** data URL or a long bare base64 blob. Short gameplay strings stay in the obfuscator. */function isRawAssetLiteral(value){if(typeof value!=="string"||value.length<64)return false;if(/^data:(?:image|audio|video|font|application)\//.test(value))return true;return value.length>=512&&/^[A-Za-z0-9+/]+=*$/.test(value)}function peelAssetLiterals(code){const blobs=[];const peeled=String(code).replace(/(['"])((?:\\.|(?!\1)[^\\])*)\1/g,(match,_quote,body)=>{const value=decodeJsString(body);if(!isRawAssetLiteral(value))return match;const index=blobs.length;blobs.push(value);return`globalThis.${RAW_ASSET_SLOT}[${index}]`});return{peeled,blobs}}function restoreAssetLiterals(code,blobs){if(!blobs||!blobs.length)return code;return`globalThis.${RAW_ASSET_SLOT}=${JSON.stringify(blobs)};${code}`}async function applyObfuscation(code,level){const n=Number(level)||0;if(n<=0||typeof code!=="string"||!code)return code;let out=code;let assetBlobs=[];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 peeled=peelAssetLiterals(out);assetBlobs=peeled.blobs;if(assetBlobs.length){const bytes=assetBlobs.reduce((sum,item)=>sum+item.length,0);console.log(`[cocos] keep ${assetBlobs.length} asset literals out of obfuscation (${bytes} chars)`)}const JavaScriptObfuscator=require("javascript-obfuscator");out=JavaScriptObfuscator.obfuscate(peeled.peeled,{...obf,reservedNames:[...(obf.reservedNames||[]),RAW_ASSET_SLOT],reservedStrings:[...(obf.reservedStrings||[]),RAW_ASSET_SLOT]}).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 restoreAssetLiterals(out,assetBlobs)}module.exports={LEVEL_NAMES,RAW_ASSET_SLOT,obfuscateLevelName,obfuscatorOptionsForLevel,obfuscatorOptionsForCocos,terserOptions,isCocosBusinessScript,isRawAssetLiteral,peelAssetLiterals,applyObfuscation};
|
|
@@ -7,4 +7,15 @@ 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("
|
|
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("keeps data urls raw and still obfuscates gameplay strings",async()=>{const blob=`data:image/png;base64,${"A".repeat(800)}`;const code=`
|
|
11
|
+
System.register("chunks:///_virtual/Demo.js", ["cc"], function (exports) {
|
|
12
|
+
return {
|
|
13
|
+
setters: [function () {}],
|
|
14
|
+
execute: function () {
|
|
15
|
+
var secret = "player-token";
|
|
16
|
+
exports("pic", function () { return "${blob}"; });
|
|
17
|
+
exports("secret", function () { return secret; });
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
`;const out=await applyObfuscation(code,2);assert.ok(out.includes(blob),"data url should stay plaintext");assert.equal(out.includes("player-token"),false);const exported={};vm.runInContext(out,vm.createContext({globalThis:global,System:{register(_name,_deps,factory){const ns=factory((key,val)=>{exported[key]=val});ns.execute()}}}),{timeout:5000});assert.equal(exported.pic(),blob);assert.equal(exported.secret(),"player-token")});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)})});
|