@playcraft/devkit 1.0.31-beta.6 → 1.0.31-beta.7

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,8 @@
1
+ "use strict";const fs=require("fs");const path=require("path");/** Overlay of /opt/cocos-cli. Not a script cache; recreating it on Linux is expensive. */const KEEP_TEMP_DIRS=new Set(["cocos-cli"]);function exists(p){try{return Boolean(p)&&fs.existsSync(p)}catch{return false}}function rmTree(dir){if(!exists(dir))return false;fs.rmSync(dir,{recursive:true,force:true});return true}/**
2
+ * Drop Creator / cocos-cli project caches so packer-driver does not reuse
3
+ * import-map + chunks that still point at another machine's paths.
4
+ *
5
+ * Keeps `temp/cocos-cli` (writable CLI overlay) unless keepCliOverlay is false.
6
+ *
7
+ * @returns {string[]} relative paths that were removed
8
+ */function cleanCocosProjectCaches(projectRoot,options={}){const keepOverlay=options.keepCliOverlay!==false;const removed=[];const library=path.join(projectRoot,"library");if(rmTree(library))removed.push("library");const tempRoot=path.join(projectRoot,"temp");if(!exists(tempRoot))return removed;if(!keepOverlay){if(rmTree(tempRoot))removed.push("temp");return removed}for(const name of fs.readdirSync(tempRoot)){if(KEEP_TEMP_DIRS.has(name))continue;if(rmTree(path.join(tempRoot,name)))removed.push(`temp/${name}`)}return removed}function shouldCleanCocosCache(buildOptions={},env=process.env){if(env.PLAYABLE_KEEP_COCOS_CACHE==="1")return false;if(env.PLAYABLE_CLEAN_COCOS_CACHE==="1")return true;const cocos=buildOptions.cocos||{};if(cocos.cleanCache===false)return false;if(cocos.cleanCache===true)return true;return Boolean(buildOptions.cleanCocosCache)}module.exports={KEEP_TEMP_DIRS,cleanCocosProjectCaches,shouldCleanCocosCache};
@@ -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{cleanCocosProjectCaches,shouldCleanCocosCache}=require("./clean-cache.js");function writeFile(file,body){fs.mkdirSync(path.dirname(file),{recursive:true});fs.writeFileSync(file,body)}describe("cleanCocosProjectCaches",()=>{let root;afterEach(()=>{if(root&&fs.existsSync(root))fs.rmSync(root,{recursive:true,force:true})});it("removes library and programming chunks but keeps cli overlay",()=>{root=fs.mkdtempSync(path.join(os.tmpdir(),"cocos-clean-cache-"));writeFile(path.join(root,"library","uuid-to-mtime.json"),"{}");writeFile(path.join(root,"temp","programming","packer-driver","targets","editor","import-map.json"),"{}");writeFile(path.join(root,"temp","builder","log.txt"),"x");writeFile(path.join(root,"temp","cocos-cli","bin","cocos"),"#!/usr/bin/env node\n");const removed=cleanCocosProjectCaches(root);assert.deepEqual(new Set(removed),new Set(["library","temp/programming","temp/builder"]));assert.equal(fs.existsSync(path.join(root,"library")),false);assert.equal(fs.existsSync(path.join(root,"temp","programming")),false);assert.equal(fs.existsSync(path.join(root,"temp","cocos-cli","bin","cocos")),true)});it("can wipe the overlay when keepCliOverlay is false",()=>{root=fs.mkdtempSync(path.join(os.tmpdir(),"cocos-clean-cache-"));writeFile(path.join(root,"temp","cocos-cli","bin","cocos"),"ok");const removed=cleanCocosProjectCaches(root,{keepCliOverlay:false});assert.deepEqual(removed,["temp"]);assert.equal(fs.existsSync(path.join(root,"temp")),false)})});describe("shouldCleanCocosCache",()=>{it("defaults off, env and cocos.cleanCache win",()=>{assert.equal(shouldCleanCocosCache({},{}),false);assert.equal(shouldCleanCocosCache({cocos:{cleanCache:true}},{}),true);assert.equal(shouldCleanCocosCache({cocos:{cleanCache:true}},{PLAYABLE_KEEP_COCOS_CACHE:"1"}),false);assert.equal(shouldCleanCocosCache({cocos:{cleanCache:false}},{PLAYABLE_CLEAN_COCOS_CACHE:"1"}),true)})});
@@ -1,4 +1,4 @@
1
- "use strict";const fs=require("fs");const path=require("path");const{spawnCocosSync,needsXvfb}=require("./cli.js");const{resolveCreatorOutputDir}=require("./creator-cli.js");const{stripAdsdkCjsImport}=require("./strip-social-share.js");const{prepareWritableCocosCli}=require("./writable-cli.js");function shouldSkipEngineBuild(buildOptions={}){const cocos=buildOptions.cocos||{};return Boolean(buildOptions.skipEngineBuild||cocos.skipEngineBuild||process.env.PLAYABLE_SKIP_ENGINE_BUILD==="1")}function resolveWebMobileDest(projectRoot,buildOptions={}){const cocos=buildOptions.cocos||{};const fromConfig=resolveCreatorOutputDir(projectRoot,cocos.buildConfig||"build-configs/web-mobile.json");const explicit=cocos.webMobileDir?path.resolve(projectRoot,cocos.webMobileDir):null;if(shouldSkipEngineBuild(buildOptions)){return explicit||fromConfig||path.resolve(projectRoot,"dist/web-mobile")}return fromConfig||explicit||path.resolve(projectRoot,"dist/web-mobile")}function logStrip(result){if(result.skipped&&result.reason==="missing"){console.warn("[cocos] adsdk CJS missing, skip social-share strip");return}if(result.skipped&&result.reason==="already"){console.log("[cocos] social-share import() already stripped");return}if(result.skipped){console.warn("[cocos] social-share import() not found in CJS");return}console.log("[cocos] stripped social-share import() from adsdk CJS")}/**
1
+ "use strict";const fs=require("fs");const path=require("path");const{spawnCocosSync,needsXvfb}=require("./cli.js");const{resolveCreatorOutputDir}=require("./creator-cli.js");const{stripAdsdkCjsImport}=require("./strip-social-share.js");const{prepareWritableCocosCli}=require("./writable-cli.js");const{cleanCocosProjectCaches,shouldCleanCocosCache}=require("./clean-cache.js");function shouldSkipEngineBuild(buildOptions={}){const cocos=buildOptions.cocos||{};return Boolean(buildOptions.skipEngineBuild||cocos.skipEngineBuild||process.env.PLAYABLE_SKIP_ENGINE_BUILD==="1")}function resolveWebMobileDest(projectRoot,buildOptions={}){const cocos=buildOptions.cocos||{};const fromConfig=resolveCreatorOutputDir(projectRoot,cocos.buildConfig||"build-configs/web-mobile.json");const explicit=cocos.webMobileDir?path.resolve(projectRoot,cocos.webMobileDir):null;if(shouldSkipEngineBuild(buildOptions)){return explicit||fromConfig||path.resolve(projectRoot,"dist/web-mobile")}return fromConfig||explicit||path.resolve(projectRoot,"dist/web-mobile")}function logStrip(result){if(result.skipped&&result.reason==="missing"){console.warn("[cocos] adsdk CJS missing, skip social-share strip");return}if(result.skipped&&result.reason==="already"){console.log("[cocos] social-share import() already stripped");return}if(result.skipped){console.warn("[cocos] social-share import() not found in CJS");return}console.log("[cocos] stripped social-share import() from adsdk CJS")}/**
2
2
  * 用官方 cocos CLI 打 web-mobile(不再走 Creator 编辑器)。
3
3
  * Linux 上自动套 xvfb-run -a(shader 编译需要虚拟显示)。
4
- */function runCocosEngineBuild(projectRoot,buildOptions={}){logStrip(stripAdsdkCjsImport(projectRoot));const dest=resolveWebMobileDest(projectRoot,buildOptions);if(shouldSkipEngineBuild(buildOptions)){if(!fs.existsSync(dest)){throw new Error(`[cocos] skipEngineBuild but missing ${path.relative(projectRoot,dest)}`)}console.log(`[cocos] skip engine build, using ${path.relative(projectRoot,dest)}`);return dest}const cocos=buildOptions.cocos||{};const configRel=cocos.buildConfig||"build-configs/web-mobile.json";const configPath=path.resolve(projectRoot,configRel);if(!fs.existsSync(configPath)){throw new Error(`[cocos] missing build config: ${configRel}`)}const args=["--no-interactive","build","--project",projectRoot,"--platform","web-mobile","--build-config",configPath];const prepared=prepareWritableCocosCli(projectRoot);const env={...process.env,...prepared.env};if(needsXvfb()){console.log("[cocos] linux, wrapping xvfb-run -a")}console.log(`[cocos] build web-mobile ← ${configRel}`);const result=spawnCocosSync(args,{cwd:projectRoot,stdio:"inherit",env});if(result.error){throw result.error}if(result.status!==0){throw new Error(`[cocos] build failed, exit ${result.status}`)}console.log(`[cocos] web-mobile ready at ${path.relative(projectRoot,dest)}`);return dest}module.exports={runCocosEngineBuild,resolveWebMobileDest,shouldSkipEngineBuild};
4
+ */function runCocosEngineBuild(projectRoot,buildOptions={}){logStrip(stripAdsdkCjsImport(projectRoot));const dest=resolveWebMobileDest(projectRoot,buildOptions);if(shouldSkipEngineBuild(buildOptions)){if(!fs.existsSync(dest)){throw new Error(`[cocos] skipEngineBuild but missing ${path.relative(projectRoot,dest)}`)}console.log(`[cocos] skip engine build, using ${path.relative(projectRoot,dest)}`);return dest}if(shouldCleanCocosCache(buildOptions)){const removed=cleanCocosProjectCaches(projectRoot);console.log(`[cocos] cleaned project caches: ${removed.join(", ")||"(none)"}`)}const cocos=buildOptions.cocos||{};const configRel=cocos.buildConfig||"build-configs/web-mobile.json";const configPath=path.resolve(projectRoot,configRel);if(!fs.existsSync(configPath)){throw new Error(`[cocos] missing build config: ${configRel}`)}const args=["--no-interactive","build","--project",projectRoot,"--platform","web-mobile","--build-config",configPath];const prepared=prepareWritableCocosCli(projectRoot);const env={...process.env,...prepared.env};if(needsXvfb()){console.log("[cocos] linux, wrapping xvfb-run -a")}console.log(`[cocos] build web-mobile ← ${configRel}`);const result=spawnCocosSync(args,{cwd:projectRoot,stdio:"inherit",env});if(result.error){throw result.error}if(result.status!==0){throw new Error(`[cocos] build failed, exit ${result.status}`)}console.log(`[cocos] web-mobile ready at ${path.relative(projectRoot,dest)}`);return dest}module.exports={runCocosEngineBuild,resolveWebMobileDest,shouldSkipEngineBuild,cleanCocosProjectCaches,shouldCleanCocosCache};
@@ -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{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)});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);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,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);await runCocosEngineBuild(projectRoot,buildOptionsBase);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,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{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)});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);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,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);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,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,4 +1,4 @@
1
- "use strict";const fs=require("fs");const path=require("path");const{spawnSync}=require("child_process");function exists(p){try{return Boolean(p)&&fs.existsSync(p)}catch{return false}}function isWritableDir(dir){try{fs.mkdirSync(dir,{recursive:true});const probe=path.join(dir,`.write-probe-${process.pid}`);fs.writeFileSync(probe,"ok");fs.unlinkSync(probe);return true}catch{return false}}function canWriteFile(file){try{fs.accessSync(file,fs.constants.W_OK);return true}catch{return false}}/** Linux 沙箱镜像里的 /opt/cocos-cli 常对 bin/temp 探测可写,但 engine 缓存与 FBX 仍落在只读树。 */function isSystemManagedCliRoot(cliRoot){if(!cliRoot)return false;if(process.env.PLAYABLE_FORCE_COCOS_OVERLAY==="1")return true;if(process.platform!=="linux")return false;const resolved=path.resolve(cliRoot);return resolved==="/opt/cocos-cli"||resolved.startsWith("/opt/cocos-cli/")}function which(bin){if(!bin)return"";if(path.isAbsolute(bin)&&exists(bin))return bin;const result=spawnSync(process.platform==="win32"?"where":"which",[bin],{encoding:"utf8"});const line=(result.stdout||"").split(/\r?\n/).map(s=>s.trim()).find(Boolean);if(!line)return"";try{return fs.realpathSync(line)}catch{return line}}function climbToCliRoot(start){let dir=start;try{if(exists(start)&&fs.statSync(start).isFile())dir=path.dirname(start)}catch{return""}for(let i=0;i<6;i++){if(exists(path.join(dir,"packages","engine","package.json"))||exists(path.join(dir,"packages","engine","cc.config.json"))||exists(path.join(dir,"packages","engine"))){return dir}const parent=path.dirname(dir);if(parent===dir)break;dir=parent}return""}function resolveCocosCliRoot(env=process.env){if(env.COCOS_CLI_ROOT&&exists(env.COCOS_CLI_ROOT)){return path.resolve(env.COCOS_CLI_ROOT)}const bin=which(env.COCOS_CLI||"cocos");if(bin){const root=climbToCliRoot(bin);if(root)return root}const fallbacks=["/opt/cocos-cli","/usr/local/cocos-cli",path.join(env.HOME||"","cocos-cli")];return fallbacks.find(p=>p&&exists(path.join(p,"packages","engine")))||""}function copyTree(src,dest){if(exists(dest))return;fs.mkdirSync(path.dirname(dest),{recursive:true});const al=spawnSync("cp",["-al",src,dest],{encoding:"utf8"});if(al.status===0&&exists(dest))return;const a=spawnSync("cp",["-a",src,dest],{encoding:"utf8"});if(a.status===0&&exists(dest))return;if(typeof fs.cpSync==="function"){fs.cpSync(src,dest,{recursive:true});return}throw new Error(`[cocos] failed to copy ${src} → ${dest}: ${(al.stderr||a.stderr||"").trim()}`)}function symlinkOrSkip(from,to){if(exists(to))return;fs.symlinkSync(from,to)}function overlayEngine(srcEngine,destEngine){fs.mkdirSync(destEngine,{recursive:true});for(const name of fs.readdirSync(srcEngine)){if(name==="bin"||name==="editor")continue;symlinkOrSkip(path.join(srcEngine,name),path.join(destEngine,name))}const srcBin=path.join(srcEngine,"bin");const srcEditor=path.join(srcEngine,"editor");if(exists(srcBin))copyTree(srcBin,path.join(destEngine,"bin"));else fs.mkdirSync(path.join(destEngine,"bin"),{recursive:true});if(exists(srcEditor))copyTree(srcEditor,path.join(destEngine,"editor"));else fs.mkdirSync(path.join(destEngine,"editor","library"),{recursive:true});fs.mkdirSync(path.join(destEngine,"bin","temp"),{recursive:true});fs.mkdirSync(path.join(destEngine,"editor","library"),{recursive:true})}function shimLinuxFbxConverter(cliRoot){if(process.platform!=="linux")return false;const binDir=path.join(cliRoot,"node_modules","@cocos","fbx-gltf-conv","bin");const linux=path.join(binDir,"linux","FBX-glTF-conv");const darwin=path.join(binDir,"darwin","FBX-glTF-conv");if(!exists(linux)||!exists(darwin))return false;try{fs.unlinkSync(darwin);fs.copyFileSync(linux,darwin);fs.chmodSync(darwin,493);return true}catch{return false}}function writeBinWrapper(destRoot){const candidates=[path.join(destRoot,"src","cli.js"),path.join(destRoot,"dist","cli.js"),path.join(destRoot,"cli.js")];const cliJs=candidates.find(exists);if(!cliJs){throw new Error(`[cocos] overlay missing cli entry under ${destRoot}`)}const binDir=path.join(destRoot,"bin");fs.mkdirSync(binDir,{recursive:true});const bin=path.join(binDir,"cocos");fs.writeFileSync(bin,`#!/usr/bin/env node\nrequire(${JSON.stringify(cliJs)});\n`);fs.chmodSync(bin,493);return bin}function overlayNodeModules(srcNm,destNm){fs.mkdirSync(destNm,{recursive:true});for(const name of fs.readdirSync(srcNm)){if(name==="@cocos")continue;symlinkOrSkip(path.join(srcNm,name),path.join(destNm,name))}const srcCocos=path.join(srcNm,"@cocos");if(!exists(srcCocos))return;const destCocos=path.join(destNm,"@cocos");fs.mkdirSync(destCocos,{recursive:true});for(const name of fs.readdirSync(srcCocos)){if(name==="fbx-gltf-conv"){copyTree(path.join(srcCocos,name),path.join(destCocos,name));continue}symlinkOrSkip(path.join(srcCocos,name),path.join(destCocos,name))}}function overlayCocosCli(srcRoot,destRoot){fs.mkdirSync(destRoot,{recursive:true});const skip=new Set(["src","dist","packages","bin","node_modules"]);for(const name of fs.readdirSync(srcRoot)){if(skip.has(name))continue;symlinkOrSkip(path.join(srcRoot,name),path.join(destRoot,name))}if(exists(path.join(srcRoot,"src")))copyTree(path.join(srcRoot,"src"),path.join(destRoot,"src"));if(exists(path.join(srcRoot,"dist")))copyTree(path.join(srcRoot,"dist"),path.join(destRoot,"dist"));if(exists(path.join(srcRoot,"node_modules"))){overlayNodeModules(path.join(srcRoot,"node_modules"),path.join(destRoot,"node_modules"))}const srcPkgs=path.join(srcRoot,"packages");const destPkgs=path.join(destRoot,"packages");fs.mkdirSync(destPkgs,{recursive:true});if(exists(srcPkgs)){for(const name of fs.readdirSync(srcPkgs)){if(name==="engine")continue;symlinkOrSkip(path.join(srcPkgs,name),path.join(destPkgs,name))}overlayEngine(path.join(srcPkgs,"engine"),path.join(destPkgs,"engine"))}const bin=writeBinWrapper(destRoot);shimLinuxFbxConverter(destRoot);return bin}function engineNeedsOverlay(cliRoot){const engine=path.join(cliRoot,"packages","engine");if(!exists(engine))return false;if(isSystemManagedCliRoot(cliRoot))return true;const library=path.join(engine,"editor","library");const internal=path.join(library,".internal");if(exists(internal)&&!canWriteFile(internal))return true;return!isWritableDir(path.join(engine,"bin","temp"))||!isWritableDir(library)}/**
1
+ "use strict";const fs=require("fs");const path=require("path");const{spawnSync}=require("child_process");function exists(p){try{return Boolean(p)&&fs.existsSync(p)}catch{return false}}function isWritableDir(dir){try{fs.mkdirSync(dir,{recursive:true});const probe=path.join(dir,`.write-probe-${process.pid}`);fs.writeFileSync(probe,"ok");fs.unlinkSync(probe);return true}catch{return false}}function which(bin){if(!bin)return"";if(path.isAbsolute(bin)&&exists(bin))return bin;const result=spawnSync(process.platform==="win32"?"where":"which",[bin],{encoding:"utf8"});const line=(result.stdout||"").split(/\r?\n/).map(s=>s.trim()).find(Boolean);if(!line)return"";try{return fs.realpathSync(line)}catch{return line}}function climbToCliRoot(start){let dir=start;try{if(exists(start)&&fs.statSync(start).isFile())dir=path.dirname(start)}catch{return""}for(let i=0;i<6;i++){if(exists(path.join(dir,"packages","engine","package.json"))||exists(path.join(dir,"packages","engine","cc.config.json"))||exists(path.join(dir,"packages","engine"))){return dir}const parent=path.dirname(dir);if(parent===dir)break;dir=parent}return""}function resolveCocosCliRoot(env=process.env){if(env.COCOS_CLI_ROOT&&exists(env.COCOS_CLI_ROOT)){return path.resolve(env.COCOS_CLI_ROOT)}const bin=which(env.COCOS_CLI||"cocos");if(bin){const root=climbToCliRoot(bin);if(root)return root}const fallbacks=["/opt/cocos-cli","/usr/local/cocos-cli",path.join(env.HOME||"","cocos-cli")];return fallbacks.find(p=>p&&exists(path.join(p,"packages","engine")))||""}function copyTree(src,dest){if(exists(dest))return;fs.mkdirSync(path.dirname(dest),{recursive:true});const al=spawnSync("cp",["-al",src,dest],{encoding:"utf8"});if(al.status===0&&exists(dest))return;const a=spawnSync("cp",["-a",src,dest],{encoding:"utf8"});if(a.status===0&&exists(dest))return;if(typeof fs.cpSync==="function"){fs.cpSync(src,dest,{recursive:true});return}throw new Error(`[cocos] failed to copy ${src} → ${dest}: ${(al.stderr||a.stderr||"").trim()}`)}function symlinkOrSkip(from,to){if(exists(to))return;fs.symlinkSync(from,to)}function overlayEngine(srcEngine,destEngine){fs.mkdirSync(destEngine,{recursive:true});for(const name of fs.readdirSync(srcEngine)){if(name==="bin"||name==="editor")continue;symlinkOrSkip(path.join(srcEngine,name),path.join(destEngine,name))}const srcBin=path.join(srcEngine,"bin");const srcEditor=path.join(srcEngine,"editor");if(exists(srcBin))copyTree(srcBin,path.join(destEngine,"bin"));else fs.mkdirSync(path.join(destEngine,"bin"),{recursive:true});if(exists(srcEditor))copyTree(srcEditor,path.join(destEngine,"editor"));else fs.mkdirSync(path.join(destEngine,"editor","library"),{recursive:true});fs.mkdirSync(path.join(destEngine,"bin","temp"),{recursive:true});fs.mkdirSync(path.join(destEngine,"editor","library"),{recursive:true})}function shimLinuxFbxConverter(cliRoot){if(process.platform!=="linux")return false;const binDir=path.join(cliRoot,"node_modules","@cocos","fbx-gltf-conv","bin");const linux=path.join(binDir,"linux","FBX-glTF-conv");const darwin=path.join(binDir,"darwin","FBX-glTF-conv");if(!exists(linux)||!exists(darwin))return false;try{fs.unlinkSync(darwin);fs.copyFileSync(linux,darwin);fs.chmodSync(darwin,493);return true}catch{return false}}function writeBinWrapper(destRoot){const candidates=[path.join(destRoot,"src","cli.js"),path.join(destRoot,"dist","cli.js"),path.join(destRoot,"cli.js")];const cliJs=candidates.find(exists);if(!cliJs){throw new Error(`[cocos] overlay missing cli entry under ${destRoot}`)}const binDir=path.join(destRoot,"bin");fs.mkdirSync(binDir,{recursive:true});const bin=path.join(binDir,"cocos");fs.writeFileSync(bin,`#!/usr/bin/env node\nrequire(${JSON.stringify(cliJs)});\n`);fs.chmodSync(bin,493);return bin}function overlayNodeModules(srcNm,destNm){fs.mkdirSync(destNm,{recursive:true});for(const name of fs.readdirSync(srcNm)){if(name==="@cocos")continue;symlinkOrSkip(path.join(srcNm,name),path.join(destNm,name))}const srcCocos=path.join(srcNm,"@cocos");if(!exists(srcCocos))return;const destCocos=path.join(destNm,"@cocos");fs.mkdirSync(destCocos,{recursive:true});for(const name of fs.readdirSync(srcCocos)){if(name==="fbx-gltf-conv"){copyTree(path.join(srcCocos,name),path.join(destCocos,name));continue}symlinkOrSkip(path.join(srcCocos,name),path.join(destCocos,name))}}function overlayCocosCli(srcRoot,destRoot){fs.mkdirSync(destRoot,{recursive:true});const skip=new Set(["src","dist","packages","bin","node_modules"]);for(const name of fs.readdirSync(srcRoot)){if(skip.has(name))continue;symlinkOrSkip(path.join(srcRoot,name),path.join(destRoot,name))}if(exists(path.join(srcRoot,"src")))copyTree(path.join(srcRoot,"src"),path.join(destRoot,"src"));if(exists(path.join(srcRoot,"dist")))copyTree(path.join(srcRoot,"dist"),path.join(destRoot,"dist"));if(exists(path.join(srcRoot,"node_modules"))){overlayNodeModules(path.join(srcRoot,"node_modules"),path.join(destRoot,"node_modules"))}const srcPkgs=path.join(srcRoot,"packages");const destPkgs=path.join(destRoot,"packages");fs.mkdirSync(destPkgs,{recursive:true});if(exists(srcPkgs)){for(const name of fs.readdirSync(srcPkgs)){if(name==="engine")continue;symlinkOrSkip(path.join(srcPkgs,name),path.join(destPkgs,name))}overlayEngine(path.join(srcPkgs,"engine"),path.join(destPkgs,"engine"))}const bin=writeBinWrapper(destRoot);shimLinuxFbxConverter(destRoot);return bin}function engineNeedsOverlay(cliRoot){const engine=path.join(cliRoot,"packages","engine");if(!exists(engine))return false;return!isWritableDir(path.join(engine,"bin","temp"))||!isWritableDir(path.join(engine,"editor","library"))}/**
2
2
  * Linux 沙箱里 /opt/cocos-cli/packages/engine 只读时,把 CLI 映到项目 temp,
3
3
  * 让 GlobalPaths.enginePath(join(__dirname, '../packages/engine'))落在可写目录。
4
- */function prepareWritableCocosCli(projectRoot,env=process.env){const srcRoot=resolveCocosCliRoot(env);if(!srcRoot){return{overlaid:false,cliRoot:"",env:{}}}if(!engineNeedsOverlay(srcRoot)){return{overlaid:false,cliRoot:srcRoot,env:{COCOS_ENGINE:path.join(srcRoot,"packages","engine")}}}const destRoot=path.join(projectRoot,"temp","cocos-cli");const destEngine=path.join(destRoot,"packages","engine");const destBin=path.join(destRoot,"bin","cocos");if(!(exists(destBin)&&exists(destEngine)&&isWritableDir(path.join(destEngine,"bin","temp")))){if(exists(destRoot))fs.rmSync(destRoot,{recursive:true,force:true});overlayCocosCli(srcRoot,destRoot)}else{shimLinuxFbxConverter(destRoot)}console.log(`[cocos] engine not writable (${srcRoot}); overlay ${path.relative(projectRoot,destRoot)}`);return{overlaid:true,cliRoot:destRoot,env:{COCOS_CLI:destBin,COCOS_CLI_ROOT:destRoot,COCOS_ENGINE:destEngine}}}module.exports={exists,isWritableDir,canWriteFile,isSystemManagedCliRoot,which,climbToCliRoot,resolveCocosCliRoot,overlayCocosCli,overlayEngine,shimLinuxFbxConverter,engineNeedsOverlay,prepareWritableCocosCli};
4
+ */function prepareWritableCocosCli(projectRoot,env=process.env){const srcRoot=resolveCocosCliRoot(env);if(!srcRoot){return{overlaid:false,cliRoot:"",env:{}}}if(!engineNeedsOverlay(srcRoot)){return{overlaid:false,cliRoot:srcRoot,env:{COCOS_ENGINE:path.join(srcRoot,"packages","engine")}}}const destRoot=path.join(projectRoot,"temp","cocos-cli");const destEngine=path.join(destRoot,"packages","engine");const destBin=path.join(destRoot,"bin","cocos");if(!(exists(destBin)&&exists(destEngine)&&isWritableDir(path.join(destEngine,"bin","temp")))){if(exists(destRoot))fs.rmSync(destRoot,{recursive:true,force:true});overlayCocosCli(srcRoot,destRoot)}else{shimLinuxFbxConverter(destRoot)}console.log(`[cocos] engine not writable (${srcRoot}); overlay ${path.relative(projectRoot,destRoot)}`);return{overlaid:true,cliRoot:destRoot,env:{COCOS_CLI:destBin,COCOS_CLI_ROOT:destRoot,COCOS_ENGINE:destEngine}}}module.exports={exists,isWritableDir,which,climbToCliRoot,resolveCocosCliRoot,overlayCocosCli,overlayEngine,shimLinuxFbxConverter,engineNeedsOverlay,prepareWritableCocosCli};
@@ -1 +1 @@
1
- "use strict";const{describe,it,before,after,beforeEach}=require("node:test");const assert=require("node:assert/strict");const fs=require("fs");const os=require("os");const path=require("path");const{overlayCocosCli,engineNeedsOverlay,prepareWritableCocosCli,shimLinuxFbxConverter}=require("./writable-cli.js");function writeFile(file,body){fs.mkdirSync(path.dirname(file),{recursive:true});fs.writeFileSync(file,body)}function makeCliTree(root){writeFile(path.join(root,"src","cli.js"),"console.log(\"cli\");\n");writeFile(path.join(root,"src","global.js"),"const { join } = require(\"path\");\nmodule.exports = { GlobalPaths: { enginePath: join(__dirname, \"..\", \"packages\", \"engine\") } };\n");writeFile(path.join(root,"packages","engine","cc.config.json"),"{}\n");writeFile(path.join(root,"packages","engine","bin","keep.txt"),"cache\n");writeFile(path.join(root,"packages","engine","editor","library",".internal"),"meta\n");writeFile(path.join(root,"packages","other","pkg.json"),"{}\n");writeFile(path.join(root,"node_modules","@cocos","fbx-gltf-conv","bin","linux","FBX-glTF-conv"),"linux-bin");writeFile(path.join(root,"node_modules","@cocos","fbx-gltf-conv","bin","darwin","FBX-glTF-conv"),"darwin-bin");writeFile(path.join(root,"bin","cocos"),"#!/usr/bin/env node\nrequire(\"../src/cli.js\");\n");fs.chmodSync(path.join(root,"bin","cocos"),493)}describe("writable cocos-cli overlay",()=>{let root;beforeEach(()=>{if(root&&fs.existsSync(root))fs.rmSync(root,{recursive:true,force:true});root=fs.mkdtempSync(path.join(os.tmpdir(),"cocos-cli-overlay-"))});after(()=>{if(root&&fs.existsSync(root))fs.rmSync(root,{recursive:true,force:true})});it("reports overlay when engine bin/temp is not writable",()=>{const cli=path.join(root,"opt-cli");makeCliTree(cli);fs.chmodSync(path.join(cli,"packages","engine","bin"),365);assert.equal(engineNeedsOverlay(cli),true);fs.chmodSync(path.join(cli,"packages","engine","bin"),493)});it("reports overlay when editor/library/.internal is not writable",()=>{const cli=path.join(root,"opt-cli");makeCliTree(cli);fs.chmodSync(path.join(cli,"packages","engine","editor","library",".internal"),292);assert.equal(engineNeedsOverlay(cli),true);fs.chmodSync(path.join(cli,"packages","engine","editor","library",".internal"),420)});it("forces overlay for PLAYABLE_FORCE_COCOS_OVERLAY",()=>{const cli=path.join(root,"opt-cli");makeCliTree(cli);const prev=process.env.PLAYABLE_FORCE_COCOS_OVERLAY;process.env.PLAYABLE_FORCE_COCOS_OVERLAY="1";try{assert.equal(engineNeedsOverlay(cli),true)}finally{if(prev===undefined)delete process.env.PLAYABLE_FORCE_COCOS_OVERLAY;else process.env.PLAYABLE_FORCE_COCOS_OVERLAY=prev}});it("maps GlobalPaths.enginePath onto a writable overlay",()=>{const cli=path.join(root,"opt-cli");makeCliTree(cli);const dest=path.join(root,"overlay");const bin=overlayCocosCli(cli,dest);assert.equal(fs.existsSync(bin),true);const enginePath=path.join(dest,"packages","engine");fs.mkdirSync(path.join(enginePath,"bin","temp"),{recursive:true});fs.writeFileSync(path.join(enginePath,"bin","temp","ok.txt"),"1");assert.equal(fs.readFileSync(path.join(enginePath,"bin","keep.txt"),"utf8"),"cache\n");const globalJs=path.join(dest,"src","global.js");delete require.cache[require.resolve(globalJs)];const resolved=require(globalJs).GlobalPaths.enginePath;assert.equal(resolved,enginePath);const srcDarwin=path.join(cli,"node_modules","@cocos","fbx-gltf-conv","bin","darwin","FBX-glTF-conv");const destDarwin=path.join(dest,"node_modules","@cocos","fbx-gltf-conv","bin","darwin","FBX-glTF-conv");assert.equal(fs.readFileSync(srcDarwin,"utf8"),"darwin-bin");if(process.platform==="linux"){assert.equal(fs.readFileSync(destDarwin,"utf8"),"linux-bin")}});it("replaces darwin FBX converter with the linux binary",()=>{const cli=path.join(root,"opt-cli");makeCliTree(cli);if(process.platform!=="linux"){const original=process.platform;Object.defineProperty(process,"platform",{value:"linux"});try{assert.equal(shimLinuxFbxConverter(cli),true)}finally{Object.defineProperty(process,"platform",{value:original})}}else{assert.equal(shimLinuxFbxConverter(cli),true)}const darwin=path.join(cli,"node_modules","@cocos","fbx-gltf-conv","bin","darwin","FBX-glTF-conv");assert.equal(fs.readFileSync(darwin,"utf8"),"linux-bin")});it("prepareWritableCocosCli overlays a locked engine into project temp",()=>{const cli=path.join(root,"opt-cli");makeCliTree(cli);fs.chmodSync(path.join(cli,"packages","engine","bin"),365);const project=path.join(root,"game");fs.mkdirSync(project);const result=prepareWritableCocosCli(project,{COCOS_CLI_ROOT:cli});assert.equal(result.overlaid,true);assert.equal(fs.existsSync(result.env.COCOS_CLI),true);fs.mkdirSync(path.join(result.env.COCOS_ENGINE,"bin","temp"),{recursive:true});fs.writeFileSync(path.join(result.env.COCOS_ENGINE,"bin","temp","ok.txt"),"1");fs.chmodSync(path.join(cli,"packages","engine","bin"),493)})});
1
+ "use strict";const{describe,it,before,after,beforeEach}=require("node:test");const assert=require("node:assert/strict");const fs=require("fs");const os=require("os");const path=require("path");const{overlayCocosCli,engineNeedsOverlay,prepareWritableCocosCli,shimLinuxFbxConverter}=require("./writable-cli.js");function writeFile(file,body){fs.mkdirSync(path.dirname(file),{recursive:true});fs.writeFileSync(file,body)}function makeCliTree(root){writeFile(path.join(root,"src","cli.js"),"console.log(\"cli\");\n");writeFile(path.join(root,"src","global.js"),"const { join } = require(\"path\");\nmodule.exports = { GlobalPaths: { enginePath: join(__dirname, \"..\", \"packages\", \"engine\") } };\n");writeFile(path.join(root,"packages","engine","cc.config.json"),"{}\n");writeFile(path.join(root,"packages","engine","bin","keep.txt"),"cache\n");writeFile(path.join(root,"packages","engine","editor","library",".internal"),"meta\n");writeFile(path.join(root,"packages","other","pkg.json"),"{}\n");writeFile(path.join(root,"node_modules","@cocos","fbx-gltf-conv","bin","linux","FBX-glTF-conv"),"linux-bin");writeFile(path.join(root,"node_modules","@cocos","fbx-gltf-conv","bin","darwin","FBX-glTF-conv"),"darwin-bin");writeFile(path.join(root,"bin","cocos"),"#!/usr/bin/env node\nrequire(\"../src/cli.js\");\n");fs.chmodSync(path.join(root,"bin","cocos"),493)}describe("writable cocos-cli overlay",()=>{let root;beforeEach(()=>{if(root&&fs.existsSync(root))fs.rmSync(root,{recursive:true,force:true});root=fs.mkdtempSync(path.join(os.tmpdir(),"cocos-cli-overlay-"))});after(()=>{if(root&&fs.existsSync(root))fs.rmSync(root,{recursive:true,force:true})});it("reports overlay when engine bin/temp is not writable",()=>{const cli=path.join(root,"opt-cli");makeCliTree(cli);fs.chmodSync(path.join(cli,"packages","engine","bin"),365);assert.equal(engineNeedsOverlay(cli),true);fs.chmodSync(path.join(cli,"packages","engine","bin"),493)});it("maps GlobalPaths.enginePath onto a writable overlay",()=>{const cli=path.join(root,"opt-cli");makeCliTree(cli);const dest=path.join(root,"overlay");const bin=overlayCocosCli(cli,dest);assert.equal(fs.existsSync(bin),true);const enginePath=path.join(dest,"packages","engine");fs.mkdirSync(path.join(enginePath,"bin","temp"),{recursive:true});fs.writeFileSync(path.join(enginePath,"bin","temp","ok.txt"),"1");assert.equal(fs.readFileSync(path.join(enginePath,"bin","keep.txt"),"utf8"),"cache\n");const globalJs=path.join(dest,"src","global.js");delete require.cache[require.resolve(globalJs)];const resolved=require(globalJs).GlobalPaths.enginePath;assert.equal(resolved,enginePath);const srcDarwin=path.join(cli,"node_modules","@cocos","fbx-gltf-conv","bin","darwin","FBX-glTF-conv");const destDarwin=path.join(dest,"node_modules","@cocos","fbx-gltf-conv","bin","darwin","FBX-glTF-conv");assert.equal(fs.readFileSync(srcDarwin,"utf8"),"darwin-bin");if(process.platform==="linux"){assert.equal(fs.readFileSync(destDarwin,"utf8"),"linux-bin")}});it("replaces darwin FBX converter with the linux binary",()=>{const cli=path.join(root,"opt-cli");makeCliTree(cli);if(process.platform!=="linux"){const original=process.platform;Object.defineProperty(process,"platform",{value:"linux"});try{assert.equal(shimLinuxFbxConverter(cli),true)}finally{Object.defineProperty(process,"platform",{value:original})}}else{assert.equal(shimLinuxFbxConverter(cli),true)}const darwin=path.join(cli,"node_modules","@cocos","fbx-gltf-conv","bin","darwin","FBX-glTF-conv");assert.equal(fs.readFileSync(darwin,"utf8"),"linux-bin")});it("prepareWritableCocosCli overlays a locked engine into project temp",()=>{const cli=path.join(root,"opt-cli");makeCliTree(cli);fs.chmodSync(path.join(cli,"packages","engine","bin"),365);const project=path.join(root,"game");fs.mkdirSync(project);const result=prepareWritableCocosCli(project,{COCOS_CLI_ROOT:cli});assert.equal(result.overlaid,true);assert.equal(fs.existsSync(result.env.COCOS_CLI),true);fs.mkdirSync(path.join(result.env.COCOS_ENGINE,"bin","temp"),{recursive:true});fs.writeFileSync(path.join(result.env.COCOS_ENGINE,"bin","temp","ok.txt"),"1");fs.chmodSync(path.join(cli,"packages","engine","bin"),493)})});
package/core/index.d.ts CHANGED
@@ -136,6 +136,8 @@ export interface CLIOptions {
136
136
  creatorBin: string;
137
137
  generatedThemeFile: string;
138
138
  enableSplash: boolean;
139
+ /** Drop library/ + temp/programming before `cocos build`. Batch builds default this on. */
140
+ cleanCache?: boolean;
139
141
  };
140
142
  }
141
143
 
package/core/options.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";const{parseArgvOptions,allowedAdProtocols,allowedAdNetworks,allowedPlaycraftOrientations,allowedLanguages,allowedEngines}=require("./utils/parseArgvOptions");const path=require("path");const fs=require("fs");const{name,version}=require("../package.json");const possibleOptions=[{name:"out-dir",alias:"outDir",defaultValue:"dist",hasValue:true,description:"Output directory for build files"},{name:"build-config",alias:"buildConfig",defaultValue:"build.json",hasValue:true,description:"Path to build.json configuration file"},{name:"ts-config",alias:"tsConfig",defaultValue:"tsconfig.json",hasValue:true,description:"For TypeScript projects, path to tsconfig.json file"},{name:"js-config",alias:"jsConfig",defaultValue:"jsconfig.json",hasValue:true,description:"For JavaScript projects, path to jsconfig.json file"},{name:"port",hasValue:true,defaultValue:3000,description:"Development server port number",parser:function(rawValue){const value=+rawValue;if(isNaN(value))throw new Error("--port should be a number");return value}},{name:"open",defaultValue:false,hasValue:false,description:"Open browser automatically when server starts"},{name:"protocol",hasValue:true,defaultValue:"none",description:"Ad protocol to use (none, mraid, or dapi)",parser:function(rawValue){if(!allowedAdProtocols.includes(rawValue)){throw new Error(`--protocol should have one of the value: ${allowedAdProtocols.join(", ")}`)}return rawValue}},{name:"network",defaultValue:"preview",hasValue:true,description:"Target Ad network",parser:function(rawValue){if(!allowedAdNetworks.includes(rawValue)){throw new Error(`--network should have one of the value: ${allowedAdNetworks.join(", ")}`)}return rawValue}},{name:"zip",defaultValue:false,hasValue:false,description:"Should the build be zipped? (only for some ad networks)"},{name:"is-channel-fold2zip",defaultValue:false,hasValue:false,description:"\u5F53\u542F\u7528\u65F6\uFF0C\u6784\u5EFA\u4EA7\u7269\u683C\u5F0F\u4E3A\u300C\u4E3B\u9898\u6587\u4EF6\u540D/\u6E20\u9053.zip\u300D"},{name:"dev",hasValue:true,description:"Enable development mode (true/false)",parser:function(rawValue){if(!(rawValue==="true"||rawValue==="false"))throw new Error("--dev should have either true or false value");return rawValue==="true"}},{name:"filename",defaultValue:"{app}_{name}_{version}_{date}_{language}_{network}",hasValue:true,description:"Specifies the build filename template"},{name:"app",defaultValue:"AppName",hasValue:true,description:"Specifies the application name used in build filename and APP define"},{name:"name",defaultValue:"ConceptName",hasValue:true,description:"Specifies the concept name used in build filename and NAME define"},{name:"version",defaultValue:"v1",hasValue:true,description:"Specifies the version name used in build filename and VERSION define"},{name:"language",defaultValue:"en",hasValue:true,description:"Specifies the language of the build used in LANGUAGE define",parser:function(rawValue){if(!allowedLanguages.includes(rawValue)){throw new Error(`--platform should have one of the value: ${allowedLanguages.join(", ")}`)}return rawValue}},{name:"playcraft-orientation",alias:"playcraftOrientation",defaultValue:["both"],hasValue:true,description:"\u5F53\u524D\u8981\u6253\u5305\u7684\u6A2A\u7AD6\u5C4F\u65B9\u5411\u7248\u672C\u5217\u8868\uFF0C\u53EF\u6307\u5B9A\u591A\u4E2A\uFF08\u9017\u53F7\u5206\u9694\uFF09\uFF0C\u53D6\u503C\uFF1Alandscape/portrait/both\u3002\u7528\u4E8E PLAYCRAFT_ORIENTATION define \u53CA window.playcraftOrientation \u8FD0\u884C\u65F6\u53D8\u91CF",parser:function(rawValue){const values=rawValue.split(",").map(v=>v.trim()).filter(Boolean);for(const value of values){if(!allowedPlaycraftOrientations.includes(value)){throw new Error(`--playcraft-orientation should have one of the value: ${allowedPlaycraftOrientations.join(", ")}`)}}return values}},{name:"playcraft-orientation-channel",alias:"playcraftOrientationChannel",defaultValue:[],hasValue:true,description:"\u54EA\u4E9B\u6E20\u9053\u9700\u8981\u6309 playcraftOrientation \u5206\u522B\u6253\u51FA\u591A\u4E2A\u65B9\u5411\u7248\u672C\u7684\u5305\uFF08\u9017\u53F7\u5206\u9694\u7684\u6E20\u9053\u540D\u5217\u8868\uFF09\u3002\u4E0D\u5728\u6B64\u5217\u8868\u4E2D\u7684\u6E20\u9053\u59CB\u7EC8\u6309 both\uFF08\u81EA\u9002\u5E94\uFF09\u5355\u4E00\u7248\u672C\u6253\u5305",parser:function(rawValue){return rawValue.split(",").map(v=>v.trim()).filter(Boolean)}},{name:"google-play-url",alias:"googlePlayUrl",defaultValue:"https://play.google.com/store/games",hasValue:true,description:"Google Play Store URL for the app"},{name:"app-store-url",alias:"appStoreUrl",defaultValue:"https://www.apple.com/app-store/",hasValue:true,description:"App Store URL for the app"},{name:"skip-recommended-meta",alias:"skipRecommendedMeta",hasValue:false,description:"Don't inject recommended for playable ads META tags"},{name:"debugger",hasValue:true,description:"URL of debugger script to inject into code"},{name:"obfuscate-level",alias:"obfuscateLevel",defaultValue:4,hasValue:true,description:"Code obfuscation level: 0=no minimize/obfuscate, 1=terser only, 2=obfuscator basic(default), 3=obfuscator+dead code, 4=maximum obfuscation",parser:function(rawValue){const value=+rawValue;if(isNaN(value)||value<0||value>4){throw new Error("--obfuscate-level should be a number between 0 and 4")}return value}},{name:"fflate-compression",alias:"fflateCompression",defaultValue:false,hasValue:true,description:"Enable fflate compression for HTML output (level 9, production only)",parser:function(rawValue){// 支持 true/false 字符串和布尔值
2
2
  if(rawValue==="true"||rawValue===true||rawValue==="1")return true;if(rawValue==="false"||rawValue===false||rawValue==="0")return false;return Boolean(rawValue)}},{name:"exclude-compress-dirs",alias:"excludeCompressDirs",defaultValue:[],hasValue:true,description:"Directories to exclude from compression (comma-separated list of paths)",parser:function(rawValue){if(!rawValue)return[];return rawValue.split(",").map(dir=>dir.trim()).filter(Boolean)}},{name:"engine",defaultValue:"",hasValue:true,description:"Playable engine: phaser, threejs, or cocos. Empty = original webpack pack",parser:function(rawValue){if(rawValue==null||rawValue==="")return"";if(!allowedEngines.includes(rawValue)){throw new Error(`--engine should have one of the value: ${allowedEngines.join(", ")} (omit or empty = webpack)`)}return rawValue}}];/** @type {import('./index').CLIOptions} */const options=parseArgvOptions(possibleOptions);// Extract CLI explicit keys set, then remove it from options
3
3
  const cliExplicitKeys=options._cliExplicitKeys||new Set;delete options._cliExplicitKeys;options.defines={};options.compilation={};options.adNetworkNames={};options.cocos={buildConfig:"build-configs/web-mobile.json",// empty = derive from Creator buildConfig buildPath + outputName
4
- webMobileDir:"",skipEngineBuild:false,creatorBin:"",generatedThemeFile:"assets/scripts/platform/theme.generated.ts",enableSplash:false};// ==================== 配置文件加载 ====================
4
+ webMobileDir:"",skipEngineBuild:false,creatorBin:"",generatedThemeFile:"assets/scripts/platform/theme.generated.ts",enableSplash:false,cleanCache:false};// ==================== 配置文件加载 ====================
5
5
  //
6
6
  // 优先级(从低到高):
7
7
  // 1. builds.config.js (新配置文件,优先读取)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/devkit",
3
- "version": "1.0.31-beta.6",
3
+ "version": "1.0.31-beta.7",
4
4
  "description": "HTML5 Playable Ads 构建工具,支持多广告渠道打包",
5
5
  "main": "core/index.js",
6
6
  "module": "core/index.js",