@playcraft/devkit 1.0.31-beta.4 → 1.0.31-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/commands/build.js +1 -1
- package/core/batch-build.js +2 -4
- package/core/engines/cocos/cli-build.js +2 -2
- package/core/engines/cocos/cli.js +1 -1
- package/core/engines/cocos/dev-server.js +2 -2
- package/core/engines/cocos/fs.js +1 -1
- package/core/engines/cocos/theme-assets.js +1 -1
- package/core/engines/cocos/writable-cli.js +4 -0
- package/core/engines/cocos/writable-cli.test.js +1 -0
- package/core/index.js +1 -1
- package/core/validators/index.d.ts +4 -0
- package/core/validators/pre-build-checker.js +1 -1
- package/core/validators/tracking-validator.js +15 -15
- package/core/validators/tracking-validator.test.js +10 -0
- package/package.json +1 -1
package/cli/commands/build.js
CHANGED
|
@@ -21,7 +21,7 @@ function handleInterrupt(){if(isExiting)return;isExiting=true;console.log("\n\x1
|
|
|
21
21
|
// 支持通过环境变量控制是否跳过检查
|
|
22
22
|
// - SKIP_PRE_BUILD_CHECKS=1: 强制跳过检查
|
|
23
23
|
// - PLAYABLE_BATCH_MODE=1: 批量构建模式,子进程自动跳过检查(主进程已执行)
|
|
24
|
-
const isBatchMode=process.env.PLAYABLE_BATCH_MODE==="1";const shouldSkipChecks=process.env.SKIP_PRE_BUILD_CHECKS==="1"||isBatchMode;async function runPreChecks(){if(shouldSkipChecks){console.log("\n\u26A0\uFE0F \u8DF3\u8FC7\u7F16\u8BD1\u524D\u68C0\u67E5\uFF08SKIP_PRE_BUILD_CHECKS=1\uFF09\n");return}const checkResult=await runPreBuildChecks(projectRoot,{verbose:false,showDetails:true,failFast:false,themes:themeName?[themeName]:undefined,channels:options["network"]?[options["network"]]:undefined});if(!checkResult.success){console.error("\n\u274C \u7F16\u8BD1\u524D\u68C0\u67E5\u5931\u8D25\uFF0C\u7EC8\u6B62\u6784\u5EFA\u3002");console.error("\u63D0\u793A\uFF1A\u5982\u9700\u8DF3\u8FC7\u68C0\u67E5\uFF0C\u53EF\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF: SKIP_PRE_BUILD_CHECKS=1\n");process.exit(1)}}// 执行编译前检查(异步)
|
|
24
|
+
const isBatchMode=process.env.PLAYABLE_BATCH_MODE==="1";const shouldSkipChecks=process.env.SKIP_PRE_BUILD_CHECKS==="1"||isBatchMode;async function runPreChecks(){if(shouldSkipChecks){console.log("\n\u26A0\uFE0F \u8DF3\u8FC7\u7F16\u8BD1\u524D\u68C0\u67E5\uFF08SKIP_PRE_BUILD_CHECKS=1\uFF09\n");return}const checkResult=await runPreBuildChecks(projectRoot,{verbose:false,showDetails:true,failFast:false,themes:themeName?[themeName]:undefined,channels:options["network"]?[options["network"]]:undefined,engine:options.engine});if(!checkResult.success){console.error("\n\u274C \u7F16\u8BD1\u524D\u68C0\u67E5\u5931\u8D25\uFF0C\u7EC8\u6B62\u6784\u5EFA\u3002");console.error("\u63D0\u793A\uFF1A\u5982\u9700\u8DF3\u8FC7\u68C0\u67E5\uFF0C\u53EF\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF: SKIP_PRE_BUILD_CHECKS=1\n");process.exit(1)}}// 执行编译前检查(异步)
|
|
25
25
|
runPreChecks().then(()=>{// 检查通过后继续原有流程
|
|
26
26
|
continueWithBuild()}).catch(err=>{console.error("\n\u274C \u7F16\u8BD1\u524D\u68C0\u67E5\u51FA\u9519:",err);process.exit(1)});function continueWithBuild(){// 当前广告渠道(用于决定是否启用 social_media 的 OG meta 注入)
|
|
27
27
|
const adNetwork=options["network"]||"";// In batch mode (PLAYABLE_BATCH_MODE=1), the parent process has already
|
package/core/batch-build.js
CHANGED
|
@@ -196,10 +196,8 @@ let channels=args.channels||config.channels;// 始终添加 preview 渠道,用
|
|
|
196
196
|
if(!channels.includes("preview")){channels=[...channels,"preview"];log("\u5DF2\u6DFB\u52A0 preview \u6E20\u9053","info")}// 根据 playcraftOrientationChannel / playcraftOrientation 配置,把需要
|
|
197
197
|
// 多方向版本的渠道拆分为独立的构建任务(渠道_L / 渠道_P / 渠道),
|
|
198
198
|
// 后续统一交给渠道级并发调度,每个方向各自独立进程构建。
|
|
199
|
-
const tasks=buildChannelTasks(channels,config)
|
|
200
|
-
|
|
201
|
-
// ============================================================
|
|
202
|
-
const shouldSkipChecks=process.env.SKIP_PRE_BUILD_CHECKS==="1";if(!shouldSkipChecks){log("\u6267\u884C\u7F16\u8BD1\u524D\u68C0\u67E5\uFF08\u57CB\u70B9\u9A8C\u8BC1\u3001\u4E3B\u9898\u540D\u79F0\u68C0\u67E5\u7B49\uFF09...","info");const checkResult=await runPreBuildChecks(projectRoot,{verbose:false,showDetails:true,failFast:false,themes,channels});if(!checkResult.success){log("\u7F16\u8BD1\u524D\u68C0\u67E5\u5931\u8D25\uFF0C\u7EC8\u6B62\u6279\u91CF\u6784\u5EFA\u3002","error");log("\u63D0\u793A\uFF1A\u5982\u9700\u8DF3\u8FC7\u68C0\u67E5\uFF0C\u53EF\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF: SKIP_PRE_BUILD_CHECKS=1","warn");process.exit(1)}log("\u2713 \u7F16\u8BD1\u524D\u68C0\u67E5\u901A\u8FC7","success")}else{log("\u8DF3\u8FC7\u7F16\u8BD1\u524D\u68C0\u67E5\uFF08SKIP_PRE_BUILD_CHECKS=1\uFF09","warn")}// 打印摘要
|
|
199
|
+
const tasks=buildChannelTasks(channels,config);// 📌 编译前检查:webpack 与 Cocos 共用,按引擎扫不同源码目录
|
|
200
|
+
const shouldSkipChecks=process.env.SKIP_PRE_BUILD_CHECKS==="1";if(!shouldSkipChecks){log("\u6267\u884C\u7F16\u8BD1\u524D\u68C0\u67E5\uFF08\u57CB\u70B9\u9A8C\u8BC1\u3001\u4E3B\u9898\u540D\u79F0\u68C0\u67E5\u7B49\uFF09...","info");const checkResult=await runPreBuildChecks(projectRoot,{verbose:false,showDetails:true,failFast:false,themes,channels,engine:config.engine});if(!checkResult.success){log("\u7F16\u8BD1\u524D\u68C0\u67E5\u5931\u8D25\uFF0C\u7EC8\u6B62\u6279\u91CF\u6784\u5EFA\u3002","error");log("\u63D0\u793A\uFF1A\u5982\u9700\u8DF3\u8FC7\u68C0\u67E5\uFF0C\u53EF\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF: SKIP_PRE_BUILD_CHECKS=1","warn");process.exit(1)}log("\u2713 \u7F16\u8BD1\u524D\u68C0\u67E5\u901A\u8FC7","success")}else{log("\u8DF3\u8FC7\u7F16\u8BD1\u524D\u68C0\u67E5\uFF08SKIP_PRE_BUILD_CHECKS=1\uFF09","warn")}if(!isWebEngine(config.engine)){log(`引擎: cocos(Creator 编一次,按主题注入 json5 / assets)`,"info");printSummary(config,themes,channels,tasks);const outputDir=path.join(projectRoot,config.build.outputDir);cleanTempDir(outputDir);const allResults=await packCocosBatch({config,themes,tasks,outputDir,projectRoot,isChannelFold2Zip});const failed=allResults.filter(r=>!r.success);const elapsed=((Date.now()-startTime)/1000).toFixed(1);log(`Cocos 批量构建完成: ${allResults.length-failed.length}/${allResults.length} 成功,耗时 ${elapsed}s`,failed.length?"warn":"success");if(failed.length&&!config.build.continueOnError)process.exit(1);return}// 打印摘要
|
|
203
201
|
printSummary(config,themes,channels,tasks);// 备份入口文件(如果启用主题切换)
|
|
204
202
|
if(config.themes.enabled){backupEntryFile(config,projectRoot)}// 批量构建
|
|
205
203
|
const outputDir=path.join(projectRoot,config.build.outputDir);const allResults=[];const previewLinks=[];// 存储预览链接
|
|
@@ -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");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");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];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
|
|
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};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";const{spawn,spawnSync}=require("child_process");function resolveCocosBin(){return
|
|
1
|
+
"use strict";const{spawn,spawnSync}=require("child_process");function resolveCocosBin(env=process.env){return env.COCOS_CLI||"cocos"}function needsXvfb(){return process.platform==="linux"}function cocosCommand(args,env=process.env){const bin=resolveCocosBin(env);if(needsXvfb()){return{cmd:"xvfb-run",args:["-a",bin,...args]}}return{cmd:bin,args}}function spawnCocos(args,options={}){const env=options.env||process.env;const{cmd,args:fullArgs}=cocosCommand(args,env);return spawn(cmd,fullArgs,options)}function spawnCocosSync(args,options={}){const env=options.env||process.env;const{cmd,args:fullArgs}=cocosCommand(args,env);return spawnSync(cmd,fullArgs,options)}module.exports={resolveCocosBin,needsXvfb,spawnCocos,spawnCocosSync};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";const fs=require("fs");const http=require("http");const path=require("path");const{spawnCocos,needsXvfb}=require("./cli.js");const{syncGeneratedTheme}=require("./theme-assets.js");const{refreshAssets}=require("./mcp-refresh.js");const{stripAdsdkCjsImport}=require("./strip-social-share.js");const{concatPreviewScripts}=require("./preview-concat.js");const{packPreviewJson}=require("./preview-json-pack.js");const{findFreePort,startPreviewProxy}=require("./preview-proxy.js");function sleep(ms){return new Promise(resolve=>setTimeout(resolve,ms))}function request(url,{method="GET",timeout=8000}={}){return new Promise((resolve,reject)=>{const req=http.request(url,{method,timeout},res=>{const chunks=[];res.on("data",c=>chunks.push(c));res.on("end",()=>{resolve({status:res.statusCode||0,body:Buffer.concat(chunks).toString("utf8")})})});req.on("timeout",()=>{req.destroy();reject(new Error(`timeout ${url}`))});req.on("error",reject);req.end()})}function resolvePreviewScene(projectRoot,explicit){if(explicit)return explicit;if(process.env.PREVIEW_SCENE)return process.env.PREVIEW_SCENE;try{const cfgPath=path.join(projectRoot,"settings/cocos.config.json");if(fs.existsSync(cfgPath)){const cfg=JSON.parse(fs.readFileSync(cfgPath,"utf8"));const fromConfig=cfg.builder&&cfg.builder.common&&cfg.builder.common.startScene||cfg.builder&&cfg.builder.platforms&&cfg.builder.platforms["web-desktop"]&&cfg.builder.platforms["web-desktop"].startScene||cfg.builder&&cfg.builder.platforms&&cfg.builder.platforms["web-mobile"]&&cfg.builder.platforms["web-mobile"].startScene;if(fromConfig)return fromConfig}}catch(_){}const scenesDir=path.join(projectRoot,"assets/scenes");if(fs.existsSync(scenesDir)){const first=fs.readdirSync(scenesDir).find(name=>name.endsWith(".scene"));if(first)return`db://assets/scenes/${first}`}return""}function killSocialShareForDev(projectRoot){const result=stripAdsdkCjsImport(projectRoot);if(result.skipped&&result.reason==="missing"){console.warn("[dev] adsdk CJS missing, skip social-share strip");return}if(result.skipped&&result.reason==="already"){console.log("[dev] social-share import() already stripped");return}if(result.skipped){console.warn("[dev] social-share import() not found in CJS");return}console.log("[dev] stripped social-share import() from adsdk CJS")}function shouldIgnoreWatch(file){if(!file)return false;const base=path.basename(file);if(base===".DS_Store"||base==="theme.generated.ts")return true;if(base.startsWith(".")&&base!==".json5")return true;const ext=path.extname(file).toLowerCase();return ext===".meta"}/**
|
|
1
|
+
"use strict";const fs=require("fs");const http=require("http");const path=require("path");const{spawnCocos,needsXvfb}=require("./cli.js");const{syncGeneratedTheme,detectCurrentTheme,themeAssetStamp}=require("./theme-assets.js");const{refreshAssets}=require("./mcp-refresh.js");const{stripAdsdkCjsImport}=require("./strip-social-share.js");const{concatPreviewScripts}=require("./preview-concat.js");const{packPreviewJson}=require("./preview-json-pack.js");const{findFreePort,startPreviewProxy}=require("./preview-proxy.js");function sleep(ms){return new Promise(resolve=>setTimeout(resolve,ms))}function request(url,{method="GET",timeout=8000}={}){return new Promise((resolve,reject)=>{const req=http.request(url,{method,timeout},res=>{const chunks=[];res.on("data",c=>chunks.push(c));res.on("end",()=>{resolve({status:res.statusCode||0,body:Buffer.concat(chunks).toString("utf8")})})});req.on("timeout",()=>{req.destroy();reject(new Error(`timeout ${url}`))});req.on("error",reject);req.end()})}function resolvePreviewScene(projectRoot,explicit){if(explicit)return explicit;if(process.env.PREVIEW_SCENE)return process.env.PREVIEW_SCENE;try{const cfgPath=path.join(projectRoot,"settings/cocos.config.json");if(fs.existsSync(cfgPath)){const cfg=JSON.parse(fs.readFileSync(cfgPath,"utf8"));const fromConfig=cfg.builder&&cfg.builder.common&&cfg.builder.common.startScene||cfg.builder&&cfg.builder.platforms&&cfg.builder.platforms["web-desktop"]&&cfg.builder.platforms["web-desktop"].startScene||cfg.builder&&cfg.builder.platforms&&cfg.builder.platforms["web-mobile"]&&cfg.builder.platforms["web-mobile"].startScene;if(fromConfig)return fromConfig}}catch(_){}const scenesDir=path.join(projectRoot,"assets/scenes");if(fs.existsSync(scenesDir)){const first=fs.readdirSync(scenesDir).find(name=>name.endsWith(".scene"));if(first)return`db://assets/scenes/${first}`}return""}function killSocialShareForDev(projectRoot){const result=stripAdsdkCjsImport(projectRoot);if(result.skipped&&result.reason==="missing"){console.warn("[dev] adsdk CJS missing, skip social-share strip");return}if(result.skipped&&result.reason==="already"){console.log("[dev] social-share import() already stripped");return}if(result.skipped){console.warn("[dev] social-share import() not found in CJS");return}console.log("[dev] stripped social-share import() from adsdk CJS")}function shouldIgnoreWatch(file){if(!file)return false;const base=path.basename(file);if(base===".DS_Store"||base==="theme.generated.ts")return true;if(base.startsWith(".")&&base!==".json5")return true;const ext=path.extname(file).toLowerCase();return ext===".meta"}/**
|
|
2
2
|
* 常驻官方 cocos CLI 预览:start-mcp-server(同一端口既有 / 也有 /mcp)。
|
|
3
|
-
*/async function runCocosDev(opts={}){const projectRoot=opts.projectRoot||process.cwd();const port=Number(opts.port||process.env.PREVIEW_PORT||3000);const timeoutMs=Number(opts.timeoutMs||process.env.DEV_TIMEOUT_MS||180000);const debounceMs=Number(opts.debounceMs||process.env.DEV_DEBOUNCE_MS||400);const cmd=opts.cmd||process.argv[2]||"start";const scene=resolvePreviewScene(projectRoot,opts.scene);const previewUrl=scene?`http://127.0.0.1:${port}/?scene=${encodeURIComponent(scene)}`:`http://127.0.0.1:${port}/`;const watchDirs=["assets","src/theme"].filter(dir=>fs.existsSync(path.join(projectRoot,dir)));async function isPreviewUp(){try{const res=await request(previewUrl,{timeout:1500});return res.status>0&&res.status<500}catch{return false}}async function waitForPreview(){const start=Date.now();let lastLog=0;while(Date.now()-start<timeoutMs){if(await isPreviewUp())return;if(Date.now()-lastLog>=5000){console.log(`[dev] waiting for ${previewUrl}`);lastLog=Date.now()}await sleep(1000)}throw new Error(`preview not ready after ${timeoutMs}ms: ${previewUrl}`)}function printBanner(reused){console.log(`[dev] ${reused?"reusing cocos CLI":"cocos CLI ready"}`);console.log(`[dev] iframe → ${previewUrl}`);console.log(`[dev] mcp → http://127.0.0.1:${port}/mcp`);console.log(`[dev] watch ${watchDirs.map(d=>`${d}/`).join(" ")}`)}function syncThemeOnce(){try{const result=syncGeneratedTheme(projectRoot);if(!result){console.log("[dev] no theme entry; skip generated module");return false}console.log(`[dev] synced theme ${result.themeName}${result.changed===false?" (unchanged)":""}`);return result.changed!==false}catch(syncErr){console.warn(`[dev] theme sync skipped: ${syncErr.message}`);return false}}async function compile({fatal=true}={}){if(!(await isPreviewUp())){console.error(`[dev] preview is down: ${previewUrl}`);if(fatal){console.error("[dev] Run npm run dev first.");process.exit(1)}return}console.log(`[dev] assets-refresh db://assets`);try{const res=await refreshAssets(port,"db://assets");const err=res.parsed&&(res.parsed.error||res.parsed.result&&res.parsed.result.isError);console.log(`[dev] ${res.status}${err?" error":""}`);if(res.status===404){console.error("[dev] /mcp \u4E0D\u5B58\u5728\u3002\u8BF7\u505C\u6389\u5F53\u524D\u9884\u89C8\uFF0C\u91CD\u65B0 npm run dev\uFF08\u4F1A\u8D77 start-mcp-server\uFF09")}if((res.status>=400||err)&&fatal)process.exit(1)}catch(err){console.error(`[dev] compile failed: ${err.message}`);if(fatal)process.exit(1)}await bundlePreviewAssets()}async function bundlePreviewAssets(){bundlePreviewScripts();try{const result=await packPreviewJson(projectRoot,{previewOrigin:`http://127.0.0.1:${port}`});if(!result)return;const kb=Math.max(1,Math.round(result.bytes/1024));console.log(`[dev] preview json ${result.files} → 1 pack (${kb} kB)`)}catch(err){console.warn(`[dev] preview json pack skipped: ${err.message}`)}}function bundlePreviewScripts(){try{const result=concatPreviewScripts(projectRoot);if(!result)return;const kb=Math.max(1,Math.round(result.bytes/1024));console.log(`[dev] preview scripts ${result.modules} → 1 bundle (${kb} kB)`)}catch(err){console.warn(`[dev] preview script concat skipped: ${err.message}`)}}function watchAndCompile(){let timer=null;let compiling=false;let pending=null;const queue=rel=>{pending=rel;if(timer)clearTimeout(timer);timer=setTimeout(()=>{timer=null;run()},debounceMs)};const run=async()=>{if(compiling)return;const rel=pending;pending=null;compiling=true;if(rel)console.log(`[dev] change ${rel}`);try{if(!rel||rel.startsWith("src/theme")){syncThemeOnce()}await compile({fatal:false})}finally{compiling=false;if(pending)queue(pending)}};for(const dir of watchDirs){const abs=path.join(projectRoot,dir);fs.watch(abs,{recursive:true},(_event,file)=>{if(shouldIgnoreWatch(file))return;const rel=path.join(dir,file||"").replace(/\\/g,"/");queue(rel)})}}function holdUntilExit(child,proxy){const stop=()=>{if(child&&child.exitCode==null){child.kill("SIGTERM")}if(proxy&&proxy.close){Promise.resolve(proxy.close()).finally(()=>process.exit(0));return}process.exit(0)};process.on("SIGINT",stop);process.on("SIGTERM",stop);if(child){child.on("exit",(code,signal)=>{console.log(`[dev] cocos CLI exited (${signal||code})`);if(proxy&&proxy.close){Promise.resolve(proxy.close()).finally(()=>process.exit(code==null?1:code));return}process.exit(code==null?1:code)});return}console.log("[dev] CLI already running; Ctrl+C leaves it open");setInterval(()=>{},60*1000)}async function start(){if(await isPreviewUp()){console.warn("[dev] reusing CLI without cache proxy; restart npm run dev to enable long-cache");printBanner(true);const synced=syncThemeOnce();if(synced)await compile({fatal:false});else await bundlePreviewAssets();watchAndCompile();holdUntilExit(null);return}if(needsXvfb()){console.log("[dev] linux, wrapping xvfb-run -a")}const cliPort=await findFreePort(port);let proxy;try{proxy=await startPreviewProxy({listenPort:port,targetPort:cliPort})}catch(err){console.error(`[dev] cache proxy failed: ${err.message}`);process.exit(1)}console.log(`[dev] cache proxy ${port} → CLI ${cliPort}`);const args=["--no-interactive","start-mcp-server","--project",projectRoot,"--port",String(cliPort)];if(scene){console.log(`[dev] start scene ${scene}`)}console.log(`[dev] cocos ${args.join(" ")}`);const child=spawnCocos(args,{cwd:projectRoot,env:process.env,stdio:"inherit"});child.on("error",err=>{console.error(err);process.exit(1)});try{await waitForPreview()}catch(err){console.error(`[dev] ${err.message}`);child.kill("SIGTERM");if(proxy&&proxy.close)await proxy.close();process.exit(1)}printBanner(false);const synced=syncThemeOnce();if(synced)await compile({fatal:false});else await bundlePreviewAssets();watchAndCompile();holdUntilExit(child,proxy)}killSocialShareForDev(projectRoot);if(cmd==="compile"||cmd==="refresh"){await compile();return}if(cmd==="start"||cmd==="dev"){await start();return}console.error(`[dev] unknown command: ${cmd}`);console.error("[dev] usage: playable-scripts dev [start|compile]");process.exit(1)}module.exports={runCocosDev};
|
|
3
|
+
*/async function runCocosDev(opts={}){const projectRoot=opts.projectRoot||process.cwd();const port=Number(opts.port||process.env.PREVIEW_PORT||3000);const timeoutMs=Number(opts.timeoutMs||process.env.DEV_TIMEOUT_MS||180000);const debounceMs=Number(opts.debounceMs||process.env.DEV_DEBOUNCE_MS||400);const cmd=opts.cmd||process.argv[2]||"start";const scene=resolvePreviewScene(projectRoot,opts.scene);const previewUrl=scene?`http://127.0.0.1:${port}/?scene=${encodeURIComponent(scene)}`:`http://127.0.0.1:${port}/`;const watchDirs=["assets","src/theme"].filter(dir=>fs.existsSync(path.join(projectRoot,dir)));async function isPreviewUp(){try{const res=await request(previewUrl,{timeout:1500});return res.status>0&&res.status<500}catch{return false}}async function waitForPreview(){const start=Date.now();let lastLog=0;while(Date.now()-start<timeoutMs){if(await isPreviewUp())return;if(Date.now()-lastLog>=5000){console.log(`[dev] waiting for ${previewUrl}`);lastLog=Date.now()}await sleep(1000)}throw new Error(`preview not ready after ${timeoutMs}ms: ${previewUrl}`)}function printBanner(reused){console.log(`[dev] ${reused?"reusing cocos CLI":"cocos CLI ready"}`);console.log(`[dev] iframe → ${previewUrl}`);console.log(`[dev] mcp → http://127.0.0.1:${port}/mcp`);console.log(`[dev] watch ${watchDirs.map(d=>`${d}/`).join(" ")}`)}function syncThemeOnce(){try{const result=syncGeneratedTheme(projectRoot);if(!result){console.log("[dev] no theme entry; skip generated module");return false}console.log(`[dev] synced theme ${result.themeName}${result.changed===false?" (unchanged)":""}`);return result.changed!==false}catch(syncErr){console.warn(`[dev] theme sync skipped: ${syncErr.message}`);return false}}async function compile({fatal=true}={}){if(!(await isPreviewUp())){console.error(`[dev] preview is down: ${previewUrl}`);if(fatal){console.error("[dev] Run npm run dev first.");process.exit(1)}return}console.log(`[dev] assets-refresh db://assets`);try{const res=await refreshAssets(port,"db://assets");const err=res.parsed&&(res.parsed.error||res.parsed.result&&res.parsed.result.isError);console.log(`[dev] ${res.status}${err?" error":""}`);if(res.status===404){console.error("[dev] /mcp \u4E0D\u5B58\u5728\u3002\u8BF7\u505C\u6389\u5F53\u524D\u9884\u89C8\uFF0C\u91CD\u65B0 npm run dev\uFF08\u4F1A\u8D77 start-mcp-server\uFF09")}if((res.status>=400||err)&&fatal)process.exit(1)}catch(err){console.error(`[dev] compile failed: ${err.message}`);if(fatal)process.exit(1)}await bundlePreviewAssets()}async function bundlePreviewAssets(){bundlePreviewScripts();try{const result=await packPreviewJson(projectRoot,{previewOrigin:`http://127.0.0.1:${port}`});if(!result)return;const kb=Math.max(1,Math.round(result.bytes/1024));console.log(`[dev] preview json ${result.files} → 1 pack (${kb} kB)`)}catch(err){console.warn(`[dev] preview json pack skipped: ${err.message}`)}}function bundlePreviewScripts(){try{const result=concatPreviewScripts(projectRoot);if(!result)return;const kb=Math.max(1,Math.round(result.bytes/1024));console.log(`[dev] preview scripts ${result.modules} → 1 bundle (${kb} kB)`)}catch(err){console.warn(`[dev] preview script concat skipped: ${err.message}`)}}function watchAndCompile(){let timer=null;let compiling=false;let pending=null;const queue=rel=>{pending=rel;if(timer)clearTimeout(timer);timer=setTimeout(()=>{timer=null;run()},debounceMs)};const run=async()=>{if(compiling)return;const rel=pending;pending=null;compiling=true;if(rel)console.log(`[dev] change ${rel}`);try{if(!rel||rel.startsWith("src/theme")||/resources\/textures\//.test(rel)){syncThemeOnce()}await compile({fatal:false})}finally{compiling=false;if(pending)queue(pending)}};for(const dir of watchDirs){const abs=path.join(projectRoot,dir);fs.watch(abs,{recursive:true},(_event,file)=>{if(shouldIgnoreWatch(file))return;const rel=path.join(dir,file||"").replace(/\\/g,"/");queue(rel)})}const themeName=detectCurrentTheme(projectRoot,{sourceDir:"src/theme",entryFile:"src/theme/index.ts"});const themeAssetsDir=themeName?path.join(projectRoot,"src/theme",themeName,"assets"):"";if(themeAssetsDir&&fs.existsSync(themeAssetsDir)){fs.watch(themeAssetsDir,()=>{queue(path.join("src/theme",themeName,"assets"))});let stamp=themeAssetStamp(themeAssetsDir);setInterval(()=>{const next=themeAssetStamp(themeAssetsDir);if(next!==stamp){stamp=next;queue(path.join("src/theme",themeName,"assets"))}},1000)}}function holdUntilExit(child,proxy){const stop=()=>{if(child&&child.exitCode==null){child.kill("SIGTERM")}if(proxy&&proxy.close){Promise.resolve(proxy.close()).finally(()=>process.exit(0));return}process.exit(0)};process.on("SIGINT",stop);process.on("SIGTERM",stop);if(child){child.on("exit",(code,signal)=>{console.log(`[dev] cocos CLI exited (${signal||code})`);if(proxy&&proxy.close){Promise.resolve(proxy.close()).finally(()=>process.exit(code==null?1:code));return}process.exit(code==null?1:code)});return}console.log("[dev] CLI already running; Ctrl+C leaves it open");setInterval(()=>{},60*1000)}async function start(){if(await isPreviewUp()){console.warn("[dev] reusing CLI without cache proxy; restart npm run dev to enable long-cache");printBanner(true);const synced=syncThemeOnce();if(synced)await compile({fatal:false});else await bundlePreviewAssets();watchAndCompile();holdUntilExit(null);return}if(needsXvfb()){console.log("[dev] linux, wrapping xvfb-run -a")}const cliPort=await findFreePort(port);let proxy;try{proxy=await startPreviewProxy({listenPort:port,targetPort:cliPort})}catch(err){console.error(`[dev] cache proxy failed: ${err.message}`);process.exit(1)}console.log(`[dev] cache proxy ${port} → CLI ${cliPort}`);const args=["--no-interactive","start-mcp-server","--project",projectRoot,"--port",String(cliPort)];if(scene){console.log(`[dev] start scene ${scene}`)}console.log(`[dev] cocos ${args.join(" ")}`);const child=spawnCocos(args,{cwd:projectRoot,env:process.env,stdio:"inherit"});child.on("error",err=>{console.error(err);process.exit(1)});try{await waitForPreview()}catch(err){console.error(`[dev] ${err.message}`);child.kill("SIGTERM");if(proxy&&proxy.close)await proxy.close();process.exit(1)}printBanner(false);const synced=syncThemeOnce();if(synced)await compile({fatal:false});else await bundlePreviewAssets();watchAndCompile();holdUntilExit(child,proxy)}killSocialShareForDev(projectRoot);if(cmd==="compile"||cmd==="refresh"){await compile();return}if(cmd==="start"||cmd==="dev"){await start();return}console.error(`[dev] unknown command: ${cmd}`);console.error("[dev] usage: playable-scripts dev [start|compile]");process.exit(1)}module.exports={runCocosDev};
|
package/core/engines/cocos/fs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";const fs=require("fs");const path=require("path");const{lookup}=require("mime-types");const{ADAPTER_FETCH,TO_STRING_EXTNAME,TO_SKIP_EXTNAME}=require("./constants");function getRealPath(pathStr){return pathStr.indexOf("\\")!==-1?pathStr.replace(/\\/g,"/"):pathStr}function getAllFilesFromDir(dirPath){let files=[];if(!fs.existsSync(dirPath))return files;for(const fileName of fs.readdirSync(dirPath)){const filePath=path.join(dirPath,fileName);const stat=fs.statSync(filePath);if(stat.isDirectory())files=files.concat(getAllFilesFromDir(filePath));else files.push(filePath)}return files}function sniffAudioMime(buf){if(!buf||buf.length<4)return"";if(buf[0]===73&&buf[1]===68&&buf[2]===51)return"audio/mpeg";if(buf[0]===255&&(buf[1]&224)===224)return"audio/mpeg";if(buf[0]===79&&buf[1]===103&&buf[2]===103&&buf[3]===83)return"audio/ogg";if(buf[0]===82&&buf[1]===73&&buf[2]===70&&buf[3]===70)return"audio/wav"
|
|
1
|
+
"use strict";const fs=require("fs");const path=require("path");const{lookup}=require("mime-types");const{ADAPTER_FETCH,TO_STRING_EXTNAME,TO_SKIP_EXTNAME}=require("./constants");function getRealPath(pathStr){return pathStr.indexOf("\\")!==-1?pathStr.replace(/\\/g,"/"):pathStr}function getAllFilesFromDir(dirPath){let files=[];if(!fs.existsSync(dirPath))return files;for(const fileName of fs.readdirSync(dirPath)){const filePath=path.join(dirPath,fileName);const stat=fs.statSync(filePath);if(stat.isDirectory())files=files.concat(getAllFilesFromDir(filePath));else files.push(filePath)}return files}function sniffImageMime(buf){if(!buf||buf.length<12){if(buf&&buf.length>=8&&buf[0]===137&&buf[1]===80&&buf[2]===78&&buf[3]===71){return"image/png"}return""}if(buf[0]===137&&buf[1]===80&&buf[2]===78&&buf[3]===71)return"image/png";if(buf[0]===255&&buf[1]===216&&buf[2]===255)return"image/jpeg";if(buf[0]===82&&buf[1]===73&&buf[2]===70&&buf[3]===70&&buf[8]===87&&buf[9]===69&&buf[10]===66&&buf[11]===80){return"image/webp"}return""}function sniffAudioMime(buf){if(!buf||buf.length<4)return"";if(buf[0]===73&&buf[1]===68&&buf[2]===51)return"audio/mpeg";if(buf[0]===255&&(buf[1]&224)===224)return"audio/mpeg";if(buf[0]===79&&buf[1]===103&&buf[2]===103&&buf[3]===83)return"audio/ogg";if(buf[0]===82&&buf[1]===73&&buf[2]===70&&buf[3]===70){if(buf.length>=12&&buf[8]===87&&buf[9]===69&&buf[10]===66&&buf[11]===80){return""}return"audio/wav"}if(buf.length>=8&&buf[4]===102&&buf[5]===116&&buf[6]===121&&buf[7]===112)return"audio/mp4";return""}function getBase64FromFile(filePath){const buf=fs.readFileSync(filePath);const lookedUp=lookup(filePath)||"";const imageMime=lookedUp.startsWith("image/")?lookedUp:sniffImageMime(buf);const mime=imageMime||(lookedUp.startsWith("audio/")?lookedUp:sniffAudioMime(buf)||lookedUp)||"application/octet-stream";return`data:${mime};base64,${buf.toString("base64")}`}function getTargetResData(filePath){const fileExtname=path.extname(filePath);if(!fileExtname)return"";if(TO_STRING_EXTNAME.includes(fileExtname)){return fs.readFileSync(filePath,"utf-8")}return getBase64FromFile(filePath)}function removeXMLHttpRequest(codeStr){return codeStr.replace(/XMLHttpRequest/g,ADAPTER_FETCH)}async function getResourceMapper({dirPath,rmHttp=false,mountCbFn,skipFiles=[]}){const resMapper={};const readPkgPath=getRealPath(`${dirPath}/`);for(const filePath of getAllFilesFromDir(dirPath)){const fileExtname=path.extname(filePath);if(TO_SKIP_EXTNAME.includes(fileExtname))continue;if(skipFiles.includes(filePath))continue;const objKey=getRealPath(filePath).replace(readPkgPath,"");let data=getTargetResData(filePath);if(mountCbFn)data=mountCbFn(objKey,data);if(rmHttp&&fileExtname===".js")data=removeXMLHttpRequest(data);resMapper[objKey]=data}return{resMapper}}module.exports={getRealPath,getAllFilesFromDir,getBase64FromFile,getTargetResData,getResourceMapper,removeXMLHttpRequest,sniffAudioMime};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
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{}}}/**
|
|
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 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 changed=writeGeneratedThemeModule(path.join(projectRoot,rel),payload);return{themeName,payload,generatedRel:rel,changed}}module.exports={detectCurrentTheme,loadThemePayload,writeGeneratedThemeModule,syncGeneratedTheme,collectDirAssets};
|
|
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};
|
|
@@ -0,0 +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)}/**
|
|
2
|
+
* Linux 沙箱里 /opt/cocos-cli/packages/engine 只读时,把 CLI 映到项目 temp,
|
|
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};
|
|
@@ -0,0 +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)})});
|
package/core/index.js
CHANGED
|
@@ -4,4 +4,4 @@ const{runPreBuildChecks,runTrackingCheck,registerChecker,toggleChecker,listCheck
|
|
|
4
4
|
exports.json5Plugin=json5Plugin;exports.assetInlinePlugin=assetInlinePlugin;exports.fontFamilyPlugin=fontFamilyPlugin;// 编译前检查器导出
|
|
5
5
|
exports.runPreBuildChecks=runPreBuildChecks;exports.runTrackingCheck=runTrackingCheck;exports.registerChecker=registerChecker;exports.toggleChecker=toggleChecker;exports.listCheckers=listCheckers;// 埋点验证器导出
|
|
6
6
|
exports.validateTracking=validateTracking;exports.printValidationResult=printValidationResult;exports.TRACKING_METHODS=TRACKING_METHODS;exports.SDK_TO_TRACKING_MAP=SDK_TO_TRACKING_MAP;// 字体注入导出
|
|
7
|
-
exports.FontInjectionWebpackPlugin=FontInjectionWebpackPlugin;exports.FONT_REGISTRY=FONT_REGISTRY;exports.getRegisteredFontKeys=getRegisteredFontKeys;exports.isBuiltinFont=isBuiltinFont;exports.getFontEntry=getFontEntry;exports.getOrGenerateFontCSS=getOrGenerateFontCSS;const{runCocosBuild,syncThemeForPreview}=require("./engines/cocos/pack.js");const{runCocosEngineBuild}=require("./engines/cocos/cli-build.js");const{syncGeneratedTheme}=require("./engines/cocos/theme-assets.js");const{refreshAssets}=require("./engines/cocos/mcp-refresh.js");const{spawnCocos,needsXvfb}=require("./engines/cocos/cli.js");const{stripAdsdkCjsImport,stripSocialShareFromDir,isSocialMediaNetwork}=require("./engines/cocos/strip-social-share.js");const{runCocosDev}=require("./engines/cocos/dev-server.js");exports.runCocosBuild=runCocosBuild;exports.syncThemeForPreview=syncThemeForPreview;exports.runCocosEngineBuild=runCocosEngineBuild;exports.syncGeneratedTheme=syncGeneratedTheme;exports.refreshAssets=refreshAssets;exports.spawnCocos=spawnCocos;exports.needsXvfb=needsXvfb;exports.stripAdsdkCjsImport=stripAdsdkCjsImport;exports.stripSocialShareFromDir=stripSocialShareFromDir;exports.isSocialMediaNetwork=isSocialMediaNetwork;exports.runCocosDev=runCocosDev;
|
|
7
|
+
exports.FontInjectionWebpackPlugin=FontInjectionWebpackPlugin;exports.FONT_REGISTRY=FONT_REGISTRY;exports.getRegisteredFontKeys=getRegisteredFontKeys;exports.isBuiltinFont=isBuiltinFont;exports.getFontEntry=getFontEntry;exports.getOrGenerateFontCSS=getOrGenerateFontCSS;const{runCocosBuild,syncThemeForPreview}=require("./engines/cocos/pack.js");const{runCocosEngineBuild}=require("./engines/cocos/cli-build.js");const{prepareWritableCocosCli}=require("./engines/cocos/writable-cli.js");const{syncGeneratedTheme}=require("./engines/cocos/theme-assets.js");const{refreshAssets}=require("./engines/cocos/mcp-refresh.js");const{spawnCocos,needsXvfb}=require("./engines/cocos/cli.js");const{stripAdsdkCjsImport,stripSocialShareFromDir,isSocialMediaNetwork}=require("./engines/cocos/strip-social-share.js");const{runCocosDev}=require("./engines/cocos/dev-server.js");exports.runCocosBuild=runCocosBuild;exports.syncThemeForPreview=syncThemeForPreview;exports.runCocosEngineBuild=runCocosEngineBuild;exports.prepareWritableCocosCli=prepareWritableCocosCli;exports.syncGeneratedTheme=syncGeneratedTheme;exports.refreshAssets=refreshAssets;exports.spawnCocos=spawnCocos;exports.needsXvfb=needsXvfb;exports.stripAdsdkCjsImport=stripAdsdkCjsImport;exports.stripSocialShareFromDir=stripSocialShareFromDir;exports.isSocialMediaNetwork=isSocialMediaNetwork;exports.runCocosDev=runCocosDev;
|
|
@@ -52,6 +52,10 @@ export interface ValidationOptions {
|
|
|
52
52
|
extensions?: string[];
|
|
53
53
|
/** 排除的目录列表 */
|
|
54
54
|
excludeDirs?: string[];
|
|
55
|
+
/** 引擎:cocos / phaser / threejs,决定扫描目录 */
|
|
56
|
+
engine?: string;
|
|
57
|
+
/** 覆盖默认扫描目录(相对项目根) */
|
|
58
|
+
scanDirs?: string[];
|
|
55
59
|
/** 是否输出详细信息 */
|
|
56
60
|
verbose?: boolean;
|
|
57
61
|
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// ============================================================
|
|
23
23
|
/**
|
|
24
24
|
* 埋点验证检查器
|
|
25
|
-
*/checkers.set("tracking",{name:"\u57CB\u70B9\u9A8C\u8BC1",description:"\u9A8C\u8BC1\u9879\u76EE\u4E2D\u662F\u5426\u6B63\u786E\u8C03\u7528\u4E86\u6240\u6709\u5FC5\u987B\u7684\u57CB\u70B9\u65B9\u6CD5",enabled:true,validator:(projectRoot,options={})=>{const result=validateTracking(projectRoot,{verbose:options.verbose||false,extensions:options.extensions,excludeDirs:options.excludeDirs});return{success:result.success,message:result.success?"\u2705 \u57CB\u70B9\u9A8C\u8BC1\u901A\u8FC7":`❌ 埋点验证失败:缺少 ${result.errors.length} 个必须的埋点方法`,data:result}}});/**
|
|
25
|
+
*/checkers.set("tracking",{name:"\u57CB\u70B9\u9A8C\u8BC1",description:"\u9A8C\u8BC1\u9879\u76EE\u4E2D\u662F\u5426\u6B63\u786E\u8C03\u7528\u4E86\u6240\u6709\u5FC5\u987B\u7684\u57CB\u70B9\u65B9\u6CD5",enabled:true,validator:(projectRoot,options={})=>{const result=validateTracking(projectRoot,{verbose:options.verbose||false,extensions:options.extensions,excludeDirs:options.excludeDirs,engine:options.engine,scanDirs:options.scanDirs});return{success:result.success,message:result.success?"\u2705 \u57CB\u70B9\u9A8C\u8BC1\u901A\u8FC7":`❌ 埋点验证失败:缺少 ${result.errors.length} 个必须的埋点方法`,data:result}}});/**
|
|
26
26
|
* 主题名称检查器
|
|
27
27
|
* 校验本次构建涉及的主题名称长度是否超过限制(不超过 50 个字符)。
|
|
28
28
|
* 主题名称过长可能导致下游文件名 / 目录名 / COS Key 超出系统或第三方平台限制。
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
*/const TRACKING_METHODS=[{name:"onPlaycraftTrackingInit",required:true,description:"SDK \u521D\u59CB\u5316\u5F00\u59CB",channels:["Bigabid:mraid_viewable","InMobi:Ad_Load_Start","AppLovin:LOADING"]},{name:"onPlaycraftTrackingLoading",required:true,description:"\u6E38\u620F\u8D44\u6E90\u52A0\u8F7D\u4E2D",channels:["Bigabid:game_viewable","InMobi:Ad_Viewable","AppLovin:LOADED"]},{name:"onPlaycraftTrackingLoaded",required:true,description:"\u4E3B\u573A\u666F\u5C31\u7EEA\uFF0C\u7528\u6237\u53EF\u4EA4\u4E92",channels:["AppLovin:DISPLAYED"]},{name:"onPlaycraftChallengeStart",required:true,description:"\u6311\u6218\u5F00\u59CB\uFF08\u9996\u6B21\u7528\u6237\u4EA4\u4E92\uFF09",channels:["Bigabid:engagement","InMobi:First_Engagement","AppLovin:CHALLENGE_STARTED"]},{name:"onPlaycraftChallengeProgress",required:false,description:"\u8FDB\u5EA6\u767E\u5206\u6BD4\u8FFD\u8E2A\uFF08\u53EF\u9009\uFF09",channels:["AppLovin:CHALLENGE_PASS_*"]},{name:"onPlaycraftChallengeSuccess",required:true,description:"\u6E38\u620F\u6311\u6218\u6210\u529F",channels:["AppLovin:CHALLENGE_SOLVED","Bigabid:complete","InMobi:Gameplay_Complete"]},{name:"onPlaycraftChallengeFinish",required:false,description:"\u6E38\u620F\u7ED3\u675F\uFF08\u53EF\u9009\uFF09",channels:["Bigabid:complete","InMobi:Gameplay_Complete","AppLovin:ENDCARD_SHOWN"]},{name:"onPlaycraftInstall",required:true,description:"\u70B9\u51FB\u5B89\u88C5",channels:["Bigabid:click","InMobi:DSP_Click","AppLovin:CTA_CLICKED"]},{name:"onPlaycraftRetry",required:false,description:"\u91CD\u8BD5\u6E38\u620F\uFF08\u53EF\u9009\uFF09",channels:["AppLovin:CHALLENGE_RETRY"]},{name:"onPlaycraftChallengeFailed",required:true,description:"\u6E38\u620F\u6311\u6218\u5931\u8D25",channels:["AppLovin:CHALLENGE_FAILED"]}];/**
|
|
15
15
|
* SDK 静态方法映射到 tracking 方法
|
|
16
16
|
* 这些 SDK 方法内部会调用对应的 tracking 方法
|
|
17
|
-
*/const SDK_TO_TRACKING_MAP={"playcraftInit":"onPlaycraftTrackingInit","playcraftStart":"onPlaycraftTrackingLoading","playcraftInteractive":"onPlaycraftTrackingLoaded","playcraftChallengeStart":"onPlaycraftChallengeStart","recordPlaycraftChallengeProgress":"onPlaycraftChallengeProgress","playcraftRecordSuccess":"onPlaycraftChallengeSuccess","playcraftFinish":"onPlaycraftChallengeFinish","playcraftInstall":"onPlaycraftInstall","playcraftRetry":"onPlaycraftRetry","playcraftRecordFailed":"onPlaycraftChallengeFailed"}
|
|
17
|
+
*/const SDK_TO_TRACKING_MAP={"playcraftInit":"onPlaycraftTrackingInit","playcraftStart":"onPlaycraftTrackingLoading","playcraftInteractive":"onPlaycraftTrackingLoaded","playcraftChallengeStart":"onPlaycraftChallengeStart","recordPlaycraftChallengeProgress":"onPlaycraftChallengeProgress","playcraftRecordSuccess":"onPlaycraftChallengeSuccess","playcraftFinish":"onPlaycraftChallengeFinish","playcraftInstall":"onPlaycraftInstall","playcraftRetry":"onPlaycraftRetry","playcraftRecordFailed":"onPlaycraftChallengeFailed"};/** Phaser / Three 游戏逻辑在 src;Cocos 在 assets/scripts。不要扫 library/temp。 */const ENGINE_SCAN_ROOTS={cocos:["assets/scripts"],phaser:["src"],threejs:["src"]};const DEFAULT_EXCLUDE_DIRS=["node_modules","dist","build",".git",".claude","library","temp","profiles","native","local","dist-all"];function detectProjectEngine(projectRoot,explicit){if(explicit)return String(explicit).toLowerCase();try{const configPath=path.join(projectRoot,"builds.config.js");if(fs.existsSync(configPath)){delete require.cache[require.resolve(configPath)];const cfg=require(configPath);if(cfg&&cfg.engine)return String(cfg.engine).toLowerCase()}}catch{// ignore invalid builds.config.js
|
|
18
|
+
}if(fs.existsSync(path.join(projectRoot,"settings/cocos.config.json")))return"cocos";if(fs.existsSync(path.join(projectRoot,"assets/scripts"))&&fs.existsSync(path.join(projectRoot,"assets/scenes"))){return"cocos"}return""}function resolveScanRoots(projectRoot,engine){const rels=ENGINE_SCAN_ROOTS[engine]||ENGINE_SCAN_ROOTS.phaser;const roots=rels.map(rel=>path.join(projectRoot,rel)).filter(dir=>fs.existsSync(dir));return roots.length?roots:[projectRoot]}function toRelScanRoots(projectRoot,roots){return roots.map(dir=>{const rel=path.relative(projectRoot,dir);return rel||"."})}// ============================================================
|
|
18
19
|
// 文件扫描工具
|
|
19
20
|
// ============================================================
|
|
20
21
|
/**
|
|
@@ -23,9 +24,9 @@
|
|
|
23
24
|
* @param {string[]} extensions - 文件扩展名列表
|
|
24
25
|
* @param {string[]} excludeDirs - 排除的目录名列表
|
|
25
26
|
* @returns {string[]} 文件路径列表
|
|
26
|
-
*/function scanSourceFiles(dir,extensions=[".ts",".tsx",".js",".jsx"],excludeDirs=
|
|
27
|
+
*/function scanSourceFiles(dir,extensions=[".ts",".tsx",".js",".jsx"],excludeDirs=DEFAULT_EXCLUDE_DIRS){const files=[];function scan(currentDir){if(!fs.existsSync(currentDir))return;const entries=fs.readdirSync(currentDir,{withFileTypes:true});for(const entry of entries){const fullPath=path.join(currentDir,entry.name);if(entry.isDirectory()){// 跳过排除的目录
|
|
27
28
|
if(excludeDirs.includes(entry.name))continue;scan(fullPath)}else if(entry.isFile()){// 检查文件扩展名
|
|
28
|
-
const ext=path.extname(entry.name);if(extensions.includes(ext)){files.push(fullPath)}}}}scan(dir);return files}/**
|
|
29
|
+
const ext=path.extname(entry.name);if(extensions.includes(ext)){files.push(fullPath)}}}}scan(dir);return files}function scanSourceFilesFromRoots(roots,extensions,excludeDirs){const files=[];const seen=new Set;for(const root of roots){for(const file of scanSourceFiles(root,extensions,excludeDirs)){if(seen.has(file))continue;seen.add(file);files.push(file)}}return files}/**
|
|
29
30
|
* 读取文件内容(带错误处理)
|
|
30
31
|
* @param {string} filePath - 文件路径
|
|
31
32
|
* @returns {string|null} 文件内容或 null
|
|
@@ -58,11 +59,9 @@ new RegExp(`\\["${sdkMethod}"\\]\\s*\\(`)];return patterns.some(pattern=>pattern
|
|
|
58
59
|
* 检测是否导入了 @playcraft/ads-tracking
|
|
59
60
|
* @param {string} content - 文件内容
|
|
60
61
|
* @returns {boolean} 是否导入了 tracking
|
|
61
|
-
*/function hasTrackingImport(content){return /from\s+['"]@playcraft\/ads-tracking['"]/.test(content)||/require\s*\(\s*['"]@playcraft\/ads-tracking['"]\s*\)/.test(content)}/**
|
|
62
|
-
* 检测是否导入了 @playcraft/adsdk
|
|
63
|
-
|
|
64
|
-
* @returns {boolean} 是否导入了 SDK
|
|
65
|
-
*/function hasSDKImport(content){return /from\s+['"]@playcraft\/adsdk['"]/.test(content)||/require\s*\(\s*['"]@playcraft\/adsdk['"]\s*\)/.test(content)}// ============================================================
|
|
62
|
+
*/function hasTrackingImport(content){return /from\s+['"]@playcraft\/ads-tracking(?:\/[^'"]*)?['"]/.test(content)||/require\s*\(\s*['"]@playcraft\/ads-tracking(?:\/[^'"]*)?['"]\s*\)/.test(content)}/**
|
|
63
|
+
* 检测是否导入了 @playcraft/adsdk(含 /cjs 等子路径,以及项目内 playcraft-adsdk 封装)
|
|
64
|
+
*/function hasSDKImport(content){return /from\s+['"]@playcraft\/adsdk(?:\/[^'"]*)?['"]/.test(content)||/require\s*\(\s*['"]@playcraft\/adsdk(?:\/[^'"]*)?['"]\s*\)/.test(content)||/from\s+['"][^'"]*playcraft-adsdk['"]/.test(content)||/require\s*\(\s*['"][^'"]*playcraft-adsdk['"]\s*\)/.test(content)}function hasAnySdkTrackingCall(content){return Object.keys(SDK_TO_TRACKING_MAP).some(sdkMethod=>detectSDKCall(content,sdkMethod))}// ============================================================
|
|
66
65
|
// 验证核心逻辑
|
|
67
66
|
// ============================================================
|
|
68
67
|
/**
|
|
@@ -80,13 +79,14 @@ new RegExp(`\\["${sdkMethod}"\\]\\s*\\(`)];return patterns.some(pattern=>pattern
|
|
|
80
79
|
* @param {Object} [options] - 验证选项
|
|
81
80
|
* @param {string[]} [options.extensions] - 文件扩展名
|
|
82
81
|
* @param {string[]} [options.excludeDirs] - 排除的目录
|
|
82
|
+
* @param {string} [options.engine] - 引擎:cocos / phaser / threejs
|
|
83
|
+
* @param {string[]} [options.scanDirs] - 覆盖默认扫描目录(相对项目根)
|
|
83
84
|
* @param {boolean} [options.verbose] - 是否输出详细信息
|
|
84
85
|
* @returns {ValidationResult} 验证结果
|
|
85
|
-
*/function validateTracking(projectRoot,options={}){const{extensions=[".ts",".tsx",".js",".jsx"],excludeDirs=
|
|
86
|
-
if(verbose)console.log(`\n[扫描]
|
|
86
|
+
*/function validateTracking(projectRoot,options={}){const{extensions=[".ts",".tsx",".js",".jsx"],excludeDirs=DEFAULT_EXCLUDE_DIRS,verbose=false}=options;const engine=detectProjectEngine(projectRoot,options.engine);const scanRoots=Array.isArray(options.scanDirs)&&options.scanDirs.length?options.scanDirs.map(dir=>path.resolve(projectRoot,dir)).filter(dir=>fs.existsSync(dir)):resolveScanRoots(projectRoot,engine);const result={success:false,integrationType:"none",methods:[],errors:[],warnings:[],stats:{totalFiles:0,scannedFiles:0,hasTrackingImport:false,hasSDKImport:false,engine,scanRoots:toRelScanRoots(projectRoot,scanRoots)}};// 1. 按引擎扫描源码目录,避免 Cocos library/temp 把检测带偏
|
|
87
|
+
if(verbose){console.log(`\n[扫描] 项目: ${projectRoot}`);console.log(`[扫描] 引擎: ${engine||"(default/src)"}`);console.log(`[扫描] 目录: ${result.stats.scanRoots.join(", ")}`)}const files=scanSourceFilesFromRoots(scanRoots,extensions,excludeDirs);result.stats.totalFiles=files.length;if(files.length===0){result.errors.push("\u672A\u627E\u5230\u4EFB\u4F55\u6E90\u7801\u6587\u4EF6");return result}if(verbose)console.log(`[扫描] 找到 ${files.length} 个源码文件\n`);// 2. 读取所有文件内容并合并(用于全局检测)
|
|
87
88
|
let allContent="";for(const file of files){const content=readFileContent(file);if(content){allContent+=content+"\n";result.stats.scannedFiles++}}// 3. 判断集成类型
|
|
88
|
-
result.stats.hasTrackingImport=hasTrackingImport(allContent);result.stats.hasSDKImport=hasSDKImport(allContent);if(result.stats.
|
|
89
|
-
}else if(result.stats.hasSDKImport){result.integrationType="sdk"}else if(result.stats.hasTrackingImport){result.integrationType="tracking"}else{result.errors.push("\u672A\u68C0\u6D4B\u5230 @playcraft/adsdk \u6216 @playcraft/ads-tracking \u7684\u5BFC\u5165");return result}if(verbose){console.log(`[集成类型] ${result.integrationType==="sdk"?"SDK \u96C6\u6210\uFF08\u901A\u8FC7 sdk \u9759\u6001\u65B9\u6CD5\u8C03\u7528\uFF09":"\u76F4\u63A5 tracking \u96C6\u6210"}`);console.log(`[导入检测] SDK: ${result.stats.hasSDKImport?"\u2713":"\u2717"} | Tracking: ${result.stats.hasTrackingImport?"\u2713":"\u2717"}\n`)}// 4. 检测每个埋点方法
|
|
89
|
+
result.stats.hasTrackingImport=hasTrackingImport(allContent);result.stats.hasSDKImport=hasSDKImport(allContent);if(result.stats.hasSDKImport||hasAnySdkTrackingCall(allContent)){result.integrationType="sdk"}else if(result.stats.hasTrackingImport){result.integrationType="tracking"}else{result.errors.push("\u672A\u68C0\u6D4B\u5230 @playcraft/adsdk \u6216 @playcraft/ads-tracking \u7684\u5BFC\u5165");return result}if(verbose){console.log(`[集成类型] ${result.integrationType==="sdk"?"SDK \u96C6\u6210\uFF08\u901A\u8FC7 sdk \u9759\u6001\u65B9\u6CD5\u8C03\u7528\uFF09":"\u76F4\u63A5 tracking \u96C6\u6210"}`);console.log(`[导入检测] SDK: ${result.stats.hasSDKImport?"\u2713":"\u2717"} | Tracking: ${result.stats.hasTrackingImport?"\u2713":"\u2717"}\n`)}// 4. 检测每个埋点方法
|
|
90
90
|
for(const method of TRACKING_METHODS){let found=false;if(result.integrationType==="sdk"){// SDK 集成:检测 SDK 方法调用
|
|
91
91
|
const sdkMethod=Object.keys(SDK_TO_TRACKING_MAP).find(key=>SDK_TO_TRACKING_MAP[key]===method.name);if(sdkMethod){found=detectSDKCall(allContent,sdkMethod)}}else if(result.integrationType==="tracking"){// 直接 tracking 集成:检测 tracking 方法调用
|
|
92
92
|
found=detectDirectTrackingCall(allContent,method.name)}result.methods.push({method:method.name,found,required:method.required,description:method.description});// 必须方法未找到 -> 错误
|
|
@@ -100,7 +100,7 @@ result.success=result.errors.length===0;return result}// =======================
|
|
|
100
100
|
* @param {ValidationResult} result - 验证结果
|
|
101
101
|
* @param {boolean} [showDetails=true] - 是否显示详细信息
|
|
102
102
|
*/function printValidationResult(result,showDetails=true){console.log("\n"+"=".repeat(60));console.log("\uD83D\uDCCA \u57CB\u70B9\u9A8C\u8BC1\u62A5\u544A");console.log("=".repeat(60));// 统计信息
|
|
103
|
-
console.log(`\n📁 文件统计: 扫描 ${result.stats.scannedFiles}/${result.stats.totalFiles} 个文件`);console.log(`🔗 集成类型: ${result.integrationType==="sdk"?"SDK \u96C6\u6210":result.integrationType==="tracking"?"Tracking \u76F4\u63A5\u96C6\u6210":"\u672A\u68C0\u6D4B\u5230"}`);// 方法检测结果
|
|
103
|
+
console.log(`\n📁 文件统计: 扫描 ${result.stats.scannedFiles}/${result.stats.totalFiles} 个文件`);if(result.stats.engine||result.stats.scanRoots&&result.stats.scanRoots.length){console.log(`🧭 引擎: ${result.stats.engine||"default"} | 目录: ${(result.stats.scanRoots||[]).join(", ")||"."}`)}console.log(`🔗 集成类型: ${result.integrationType==="sdk"?"SDK \u96C6\u6210":result.integrationType==="tracking"?"Tracking \u76F4\u63A5\u96C6\u6210":"\u672A\u68C0\u6D4B\u5230"}`);// 方法检测结果
|
|
104
104
|
if(showDetails){console.log("\n\uD83D\uDCCB \u65B9\u6CD5\u68C0\u6D4B\u660E\u7EC6:");console.log("-".repeat(60));const requiredMethods=result.methods.filter(m=>m.required);const optionalMethods=result.methods.filter(m=>!m.required);console.log("\n\u5FC5\u987B\u65B9\u6CD5:");for(const m of requiredMethods){const status=m.found?"\u2705":"\u274C";console.log(` ${status} ${m.method.padEnd(35)} ${m.found?"\u5DF2\u8C03\u7528":"\u672A\u8C03\u7528"}`)}console.log("\n\u53EF\u9009\u65B9\u6CD5:");for(const m of optionalMethods){const status=m.found?"\u2705":"\u26A0\uFE0F ";console.log(` ${status} ${m.method.padEnd(35)} ${m.found?"\u5DF2\u8C03\u7528":"\u672A\u8C03\u7528"}`)}}// 错误信息
|
|
105
105
|
if(result.errors.length>0){console.log("\n\u274C \u9519\u8BEF:");result.errors.forEach(err=>console.log(` ${err}`))}// 警告信息
|
|
106
106
|
if(result.warnings.length>0){console.log("\n\u26A0\uFE0F \u8B66\u544A:");result.warnings.forEach(warn=>console.log(` ${warn}`))}// 最终结果
|
|
@@ -109,5 +109,5 @@ console.log("\n"+"=".repeat(60));if(result.success){console.log("\u2705 \u9A8C\u
|
|
|
109
109
|
// ============================================================
|
|
110
110
|
module.exports={// 核心验证函数
|
|
111
111
|
validateTracking,printValidationResult,// 配置数据(供外部使用)
|
|
112
|
-
TRACKING_METHODS,SDK_TO_TRACKING_MAP,// 工具函数(供外部使用)
|
|
113
|
-
scanSourceFiles,detectDirectTrackingCall,detectSDKCall,hasTrackingImport,hasSDKImport};
|
|
112
|
+
TRACKING_METHODS,SDK_TO_TRACKING_MAP,ENGINE_SCAN_ROOTS,DEFAULT_EXCLUDE_DIRS,// 工具函数(供外部使用)
|
|
113
|
+
detectProjectEngine,resolveScanRoots,scanSourceFiles,detectDirectTrackingCall,detectSDKCall,hasTrackingImport,hasSDKImport};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";const{describe,it,before,after}=require("node:test");const assert=require("node:assert/strict");const fs=require("fs");const os=require("os");const path=require("path");const{validateTracking,detectProjectEngine,resolveScanRoots,hasSDKImport}=require("./tracking-validator.js");const SDK_SOURCE=`
|
|
2
|
+
import { sdk } from '@playcraft/adsdk/cjs';
|
|
3
|
+
sdk.playcraftInit(() => {});
|
|
4
|
+
sdk.playcraftStart();
|
|
5
|
+
sdk.playcraftInteractive();
|
|
6
|
+
sdk.playcraftChallengeStart();
|
|
7
|
+
sdk.playcraftRecordSuccess();
|
|
8
|
+
sdk.playcraftRecordFailed();
|
|
9
|
+
sdk.playcraftInstall();
|
|
10
|
+
`;describe("tracking validator engine scan roots",()=>{let root;before(()=>{root=fs.mkdtempSync(path.join(os.tmpdir(),"tracking-scan-"));fs.mkdirSync(path.join(root,"assets/scripts/game"),{recursive:true});fs.mkdirSync(path.join(root,"assets/scenes"),{recursive:true});fs.mkdirSync(path.join(root,"library/import"),{recursive:true});fs.mkdirSync(path.join(root,"src"),{recursive:true});fs.writeFileSync(path.join(root,"builds.config.js"),"module.exports = { engine: 'cocos' };\n");fs.writeFileSync(path.join(root,"assets/scripts/game/GameRoot.ts"),SDK_SOURCE);fs.writeFileSync(path.join(root,"library/import/noise.js"),"import { tracking } from '@playcraft/ads-tracking';\ntracking.onPlaycraftTrackingInit();\n");fs.writeFileSync(path.join(root,"src/index.ts"),"export const unused = 1;\n")});after(()=>{fs.rmSync(root,{recursive:true,force:true})});it("treats @playcraft/adsdk/cjs as an SDK import",()=>{assert.equal(hasSDKImport("import * as adsdk from '@playcraft/adsdk/cjs';"),true);assert.equal(hasSDKImport("import { sdk } from '@playcraft/adsdk';"),true)});it("detects cocos from builds.config.js and scans assets/scripts only",()=>{assert.equal(detectProjectEngine(root),"cocos");const roots=resolveScanRoots(root,"cocos").map(dir=>path.relative(root,dir));assert.deepEqual(roots,["assets/scripts"])});it("passes required tracking checks for a cocos SDK wrapper project",()=>{const result=validateTracking(root,{engine:"cocos"});assert.equal(result.stats.engine,"cocos");assert.deepEqual(result.stats.scanRoots,["assets/scripts"]);assert.equal(result.integrationType,"sdk");assert.equal(result.success,true,result.errors.join("\n"));assert.equal(result.stats.hasSDKImport,true)});it("does not let library ads-tracking imports flip the integration type",()=>{const result=validateTracking(root,{engine:"cocos"});assert.equal(result.stats.hasTrackingImport,false);assert.ok(result.stats.scannedFiles<10)});it("scans src for phaser",()=>{const phaserRoot=fs.mkdtempSync(path.join(os.tmpdir(),"tracking-phaser-"));fs.mkdirSync(path.join(phaserRoot,"src"),{recursive:true});fs.mkdirSync(path.join(phaserRoot,"library"),{recursive:true});fs.writeFileSync(path.join(phaserRoot,"src/main.ts"),SDK_SOURCE.replace("/cjs",""));fs.writeFileSync(path.join(phaserRoot,"library/noise.js"),"import { tracking } from '@playcraft/ads-tracking';\n");try{const result=validateTracking(phaserRoot,{engine:"phaser"});assert.deepEqual(result.stats.scanRoots,["src"]);assert.equal(result.integrationType,"sdk");assert.equal(result.success,true,result.errors.join("\n"))}finally{fs.rmSync(phaserRoot,{recursive:true,force:true})}})});
|