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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/cli/bin/playable-scripts.js +6 -53
  2. package/cli/commands/build.js +32 -241
  3. package/cli/commands/builds.js +2 -13
  4. package/cli/commands/dev.js +1 -22
  5. package/cli/commands/pack.js +40 -320
  6. package/cli/commands/vite-dev.js +1 -7
  7. package/core/artifacts/emit-fonts.test.js +1 -0
  8. package/core/artifacts/emit.js +4 -98
  9. package/core/artifacts/fflate.js +1 -31
  10. package/core/artifacts/zip.js +1 -39
  11. package/core/batch-build.js +147 -1261
  12. package/core/constants.js +2 -20
  13. package/core/cos-uploader.js +23 -289
  14. package/core/engines/cocos/adapter-vfs-path.test.js +1 -0
  15. package/core/engines/cocos/clean-cache.js +5 -179
  16. package/core/engines/cocos/clean-cache.test.js +1 -75
  17. package/core/engines/cocos/cli-build.js +2 -116
  18. package/core/engines/cocos/cli.js +11 -38
  19. package/core/engines/cocos/constants.js +1 -21
  20. package/core/engines/cocos/creator-cli.js +1 -108
  21. package/core/engines/cocos/dev-server.js +4 -412
  22. package/core/engines/cocos/fs.js +1 -105
  23. package/core/engines/cocos/injects/3x/init.js +19 -438
  24. package/core/engines/cocos/injects/3x/main.js +2 -52
  25. package/core/engines/cocos/injects/libs/pako.js +1 -2
  26. package/core/engines/cocos/mcp-refresh.js +2 -76
  27. package/core/engines/cocos/merge-inline.test.js +1 -0
  28. package/core/engines/cocos/merge.js +9 -148
  29. package/core/engines/cocos/omit-playable-weight.js +10 -0
  30. package/core/engines/cocos/omit-playable-weight.test.js +2 -0
  31. package/core/engines/cocos/pack.js +1 -187
  32. package/core/engines/cocos/preview-cache.js +2 -48
  33. package/core/engines/cocos/preview-cache.test.js +1 -32
  34. package/core/engines/cocos/preview-concat.js +6 -189
  35. package/core/engines/cocos/preview-concat.test.js +1 -73
  36. package/core/engines/cocos/preview-json-hook.js +1 -200
  37. package/core/engines/cocos/preview-json-pack.js +3 -376
  38. package/core/engines/cocos/preview-json-pack.test.js +2 -226
  39. package/core/engines/cocos/preview-proxy.js +1 -132
  40. package/core/engines/cocos/rebundle-preview.js +2 -54
  41. package/core/engines/cocos/strip-social-share.js +3 -115
  42. package/core/engines/cocos/theme-assets.js +2 -161
  43. package/core/engines/cocos/theme-levels.js +1 -0
  44. package/core/engines/cocos/writable-cli.js +2 -248
  45. package/core/engines/cocos/writable-cli.test.js +1 -112
  46. package/core/engines/web/index.js +1 -20
  47. package/core/engines/web/vite.dev.js +17 -322
  48. package/core/engines/web/webpack.build.js +37 -522
  49. package/core/engines/web/webpack.common.js +11 -177
  50. package/core/engines/web/webpack.dev.js +6 -88
  51. package/core/index.js +7 -101
  52. package/core/injectors/apply.js +1 -94
  53. package/core/injectors/check-channel-matrix.js +2 -130
  54. package/core/injectors/spec.js +10 -217
  55. package/core/loaders/gltf-loader.js +28 -74
  56. package/core/options.js +16 -353
  57. package/core/plugins/AdikteevInjectorPlugin.js +4 -26
  58. package/core/plugins/BigoAdsInjectorPlugin.js +4 -21
  59. package/core/plugins/DAPIInjectorPlugin.js +2 -25
  60. package/core/plugins/DebuggerInjectionPlugin.js +3 -31
  61. package/core/plugins/ExitAPIInjectorPlugin.js +3 -56
  62. package/core/plugins/FflateCompressionPlugin.js +160 -307
  63. package/core/plugins/FflateCompressionPlugin.test.js +13 -0
  64. package/core/plugins/FontInjectionWebpackPlugin.js +4 -46
  65. package/core/plugins/LiftoffInjectorPlugin.js +4 -26
  66. package/core/plugins/MRAIDInjectorPlugin.js +2 -24
  67. package/core/plugins/MintegralInjectorPlugin.js +2 -25
  68. package/core/plugins/PangleInjectorPlugin.js +2 -24
  69. package/core/plugins/PlaycraftOrientationInjectorPlugin.js +3 -29
  70. package/core/plugins/SnapchatInjectorPlugin.js +4 -26
  71. package/core/plugins/SocialMediaInjectorPlugin.js +7 -119
  72. package/core/plugins/TikTokInjectorPlugin.js +2 -24
  73. package/core/plugins/UnityInjectorPlugin.js +8 -37
  74. package/core/plugins/ZipPlugin.js +20 -154
  75. package/core/plugins/socialMediaVitePlugin.js +2 -108
  76. package/core/utils/buildDefines.js +2 -30
  77. package/core/utils/buildTemplateString.js +5 -39
  78. package/core/utils/date.js +1 -16
  79. package/core/utils/fontRegistry.js +13 -81
  80. package/core/utils/fontResolver.js +37 -335
  81. package/core/utils/generateAdikteevHtmlWebpackPluginConfig.js +8 -42
  82. package/core/utils/generateBigabidHtmlWebpackPluginConfig.js +3 -30
  83. package/core/utils/generateInMobiHtmlWebpackPluginConfig.js +7 -43
  84. package/core/utils/getPlaycraftOrientationSuffix.js +2 -9
  85. package/core/utils/injectSDKPlugins.js +13 -68
  86. package/core/utils/logOptions.js +5 -46
  87. package/core/utils/mergeOptions.js +2 -21
  88. package/core/utils/obfuscateLevels.js +14 -0
  89. package/core/utils/obfuscateLevels.test.js +10 -0
  90. package/core/utils/parseArgvOptions.js +5 -125
  91. package/core/utils/resolveChannelFold2Zip.js +3 -15
  92. package/core/utils/validateThemeData.js +26 -227
  93. package/core/validators/pre-build-checker.js +25 -287
  94. package/core/validators/tracking-validator.js +40 -441
  95. package/core/validators/tracking-validator.test.js +2 -84
  96. package/core/vite.dev.js +1 -1
  97. package/core/webpack.build.js +1 -1
  98. package/core/webpack.common.js +1 -1
  99. package/core/webpack.dev.js +1 -1
  100. package/package.json +2 -3
  101. package/README.md +0 -262
package/core/constants.js CHANGED
@@ -1,24 +1,6 @@
1
1
  #!/usr/bin/env node
2
-
3
- 'use strict';
4
-
5
- /**
2
+ "use strict";/**
6
3
  * 全局常量配置
7
4
  *
8
5
  * 集中管理服务端 API 地址、构建参数等常量,方便切换环境。
9
- */
10
-
11
- /** 服务端 API 基础地址(主) */
12
- const API_BASE_URL = 'http://9.135.99.122';
13
-
14
- /** 服务端 API 基础地址(备用),主地址不可用时 fallback */
15
- const API_BASE_URL_FALLBACK = 'https://wd-act.woa.com';
16
-
17
- /** 可试玩广告产物大小上限(Facebook ZIP 与其余 ZIP 渠道一致) */
18
- const MAX_PLAYABLE_PACKAGE_BYTES = 5 * 1024 * 1024;
19
-
20
- module.exports = {
21
- API_BASE_URL,
22
- API_BASE_URL_FALLBACK,
23
- MAX_PLAYABLE_PACKAGE_BYTES,
24
- };
6
+ *//** 服务端 API 基础地址(主) */const API_BASE_URL="http://9.135.99.122";/** 服务端 API 基础地址(备用),主地址不可用时 fallback */const API_BASE_URL_FALLBACK="https://wd-act.woa.com";/** 可试玩广告产物大小上限(Facebook ZIP 与其余 ZIP 渠道一致) */const MAX_PLAYABLE_PACKAGE_BYTES=5*1024*1024;module.exports={API_BASE_URL,API_BASE_URL_FALLBACK,MAX_PLAYABLE_PACKAGE_BYTES};
@@ -1,146 +1,25 @@
1
1
  #!/usr/bin/env node
2
-
3
- 'use strict';
4
-
5
- /**
2
+ "use strict";/**
6
3
  * COS 上传模块
7
4
  *
8
5
  * 功能:上传文件到腾讯云 COS,并刷新 CDN 缓存
9
- */
10
-
11
- const COS = require('cos-nodejs-sdk-v5');
12
- const fs = require('fs');
13
- const path = require('path');
14
- const http = require('http');
15
- const https = require('https');
16
- const { API_BASE_URL, API_BASE_URL_FALLBACK } = require('./constants');
17
-
18
- /**
6
+ */const COS=require("cos-nodejs-sdk-v5");const fs=require("fs");const path=require("path");const http=require("http");const https=require("https");const{API_BASE_URL,API_BASE_URL_FALLBACK}=require("./constants");/**
19
7
  * 通用 API 请求(自动适配 http/https,支持失败 fallback)
20
8
  * @param {string} path - API 路径,如 '/aix/playable/api/cos/credentials'
21
9
  * @param {string} method - 请求方法 GET/POST
22
10
  * @param {Object|null} postData - POST 请求体(JSON 对象)
23
11
  * @returns {Promise<Object>} - 解析后的 JSON 响应
24
- */
25
- async function apiRequest(path, method = 'GET', postData = null) {
26
- const urls = [
27
- `${API_BASE_URL}${path}`,
28
- `${API_BASE_URL_FALLBACK}${path}`,
29
- ];
30
-
31
- let lastError = null;
32
-
33
- for (const url of urls) {
34
- try {
35
- const result = await new Promise((resolve, reject) => {
36
- const isHttps = url.startsWith('https');
37
- const transport = isHttps ? https : http;
38
-
39
- const doRequest = (res) => {
40
- if (res.statusCode !== 200) {
41
- reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
42
- return;
43
- }
44
- let data = '';
45
- res.on('data', chunk => data += chunk);
46
- res.on('end', () => {
47
- try {
48
- const result = JSON.parse(data);
49
- if (result.code === 'SUCCESS') {
50
- resolve(result);
51
- } else {
52
- reject(new Error(result.message || 'API request failed'));
53
- }
54
- } catch (error) {
55
- reject(new Error(`Failed to parse response: ${error.message}`));
56
- }
57
- });
58
- };
59
-
60
- let req;
61
- if (method === 'POST' && postData) {
62
- const body = JSON.stringify(postData);
63
- req = transport.request(url, {
64
- method: 'POST',
65
- headers: {
66
- 'Content-Type': 'application/json',
67
- 'Content-Length': Buffer.byteLength(body),
68
- },
69
- }, doRequest);
70
- req.write(body);
71
- } else {
72
- req = transport.get(url, doRequest);
73
- }
74
-
75
- req.on('error', reject);
76
- req.setTimeout(30000, function () {
77
- this.destroy();
78
- reject(new Error('Request timeout'));
79
- });
80
-
81
- if (method === 'GET') {
82
- req.end();
83
- } else if (method === 'POST' && !postData) {
84
- req.end();
85
- }
86
- });
87
-
88
- // 成功则直接返回
89
- return result;
90
- } catch (error) {
91
- lastError = error;
92
- // 如果还有备用 URL,继续尝试
93
- if (url !== urls[urls.length - 1]) {
94
- console.warn(`⚠️ 主域名请求失败 (${url}), 尝试备用域名...`);
95
- continue;
96
- }
97
- }
98
- }
99
-
100
- throw lastError || new Error('All API endpoints failed');
101
- }
102
-
103
- /**
12
+ */async function apiRequest(path,method="GET",postData=null){const urls=[`${API_BASE_URL}${path}`,`${API_BASE_URL_FALLBACK}${path}`];let lastError=null;for(const url of urls){try{const result=await new Promise((resolve,reject)=>{const isHttps=url.startsWith("https");const transport=isHttps?https:http;const doRequest=res=>{if(res.statusCode!==200){reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));return}let data="";res.on("data",chunk=>data+=chunk);res.on("end",()=>{try{const result=JSON.parse(data);if(result.code==="SUCCESS"){resolve(result)}else{reject(new Error(result.message||"API request failed"))}}catch(error){reject(new Error(`Failed to parse response: ${error.message}`))}})};let req;if(method==="POST"&&postData){const body=JSON.stringify(postData);req=transport.request(url,{method:"POST",headers:{"Content-Type":"application/json","Content-Length":Buffer.byteLength(body)}},doRequest);req.write(body)}else{req=transport.get(url,doRequest)}req.on("error",reject);req.setTimeout(30000,function(){this.destroy();reject(new Error("Request timeout"))});if(method==="GET"){req.end()}else if(method==="POST"&&!postData){req.end()}});// 成功则直接返回
13
+ return result}catch(error){lastError=error;// 如果还有备用 URL,继续尝试
14
+ if(url!==urls[urls.length-1]){console.warn(`⚠️ 主域名请求失败 (${url}), 尝试备用域名...`);continue}}}throw lastError||new Error("All API endpoints failed")}/**
104
15
  * 从远程 API 获取临时凭证(支持主/备域名 fallback)
105
- */
106
- async function fetchTemporaryCredentials() {
107
- const result = await apiRequest('/aix/playable/api/cos/credentials');
108
- return result.data;
109
- }
110
-
111
- /**
16
+ */async function fetchTemporaryCredentials(){const result=await apiRequest("/aix/playable/api/cos/credentials");return result.data}/**
112
17
  * 创建 COS 客户端(使用临时凭证)
113
18
  * @param {Object} cosConfig - COS 配置
114
19
  * @param {Object} credentials - 已获取的临时凭证(可选,避免重复请求)
115
- */
116
- async function createCOSClient(cosConfig, credentials = null) {
117
- // 如果配置中提供了 SecretId 和 SecretKey,使用传统方式
118
- if (cosConfig && cosConfig.SecretId && cosConfig.SecretKey) {
119
- return new COS({
120
- SecretId: cosConfig.SecretId,
121
- SecretKey: cosConfig.SecretKey,
122
- });
123
- }
124
-
125
- // 使用已传入的凭证,或重新获取
126
- if (!credentials) {
127
- credentials = await fetchTemporaryCredentials();
128
- }
129
-
130
- return new COS({
131
- getAuthorization: (options, callback) => {
132
- callback({
133
- TmpSecretId: credentials.credentials.tmpSecretId,
134
- TmpSecretKey: credentials.credentials.tmpSecretKey,
135
- SecurityToken: credentials.credentials.sessionToken,
136
- StartTime: credentials.startTime,
137
- ExpiredTime: credentials.expiredTime,
138
- });
139
- },
140
- });
141
- }
142
-
143
- /**
20
+ */async function createCOSClient(cosConfig,credentials=null){// 如果配置中提供了 SecretId 和 SecretKey,使用传统方式
21
+ if(cosConfig&&cosConfig.SecretId&&cosConfig.SecretKey){return new COS({SecretId:cosConfig.SecretId,SecretKey:cosConfig.SecretKey})}// 使用已传入的凭证,或重新获取
22
+ if(!credentials){credentials=await fetchTemporaryCredentials()}return new COS({getAuthorization:(options,callback)=>{callback({TmpSecretId:credentials.credentials.tmpSecretId,TmpSecretKey:credentials.credentials.tmpSecretKey,SecurityToken:credentials.credentials.sessionToken,StartTime:credentials.startTime,ExpiredTime:credentials.expiredTime})}})}/**
144
23
  * 刷新 CDN 缓存
145
24
  * 通过服务端 API 代理执行(STS 临时凭证无法携带 CDN 权限,需由服务端使用主账号凭证刷新)
146
25
  *
@@ -148,49 +27,9 @@ async function createCOSClient(cosConfig, credentials = null) {
148
27
  * @param {Array<string>} urls - 需要刷新的 URL 列表
149
28
  * @param {Object} credentials - 未使用,保留参数兼容性
150
29
  * @returns {Promise<void>}
151
- */
152
- async function purgeCDNCache(cosConfig, urls, credentials = null) {
153
- if (!urls || urls.length === 0) {
154
- return;
155
- }
156
-
157
- const area = cosConfig?.CdnArea || 'overseas';
158
-
159
- // 如果配置中有 SecretId/SecretKey,本地直接刷新
160
- if (cosConfig && cosConfig.SecretId && cosConfig.SecretKey) {
161
- try {
162
- const tencentcloud = require('tencentcloud-sdk-nodejs-cdn');
163
- const client = new tencentcloud.cdn.v20180606.Client({
164
- credential: { secretId: cosConfig.SecretId, secretKey: cosConfig.SecretKey },
165
- region: '',
166
- profile: { httpProfile: { endpoint: 'cdn.tencentcloudapi.com' } },
167
- });
168
- const batchSize = 1000;
169
- for (let i = 0; i < urls.length; i += batchSize) {
170
- const batch = urls.slice(i, i + batchSize);
171
- await client.PurgeUrlsCache({ Urls: batch, Area: area });
172
- console.log(`✓ CDN 缓存已刷新 ${batch.length} 个 URL (区域: ${area})`);
173
- }
174
- } catch (error) {
175
- if (error.code === 'MODULE_NOT_FOUND') {
176
- console.warn('⚠️ 未安装 tencentcloud-sdk-nodejs-cdn,跳过 CDN 刷新');
177
- } else {
178
- throw error;
179
- }
180
- }
181
- return;
182
- }
183
-
184
- // 否则通过服务端 API 代理刷新(支持主/备域名 fallback)
185
- try {
186
- await apiRequest('/aix/playable/api/cdn/purge', 'POST', { urls, area });
187
- console.log(`✓ CDN 缓存已刷新 ${urls.length} 个 URL (区域: ${area})`);
188
- } catch (error) {
189
- throw new Error(`CDN purge failed: ${error.message}`);
190
- }
191
- }
192
-
193
- /**
30
+ */async function purgeCDNCache(cosConfig,urls,credentials=null){if(!urls||urls.length===0){return}const area=(cosConfig===null||cosConfig===void 0?void 0:cosConfig.CdnArea)||"overseas";// 如果配置中有 SecretId/SecretKey,本地直接刷新
31
+ if(cosConfig&&cosConfig.SecretId&&cosConfig.SecretKey){try{const tencentcloud=require("tencentcloud-sdk-nodejs-cdn");const client=new tencentcloud.cdn.v20180606.Client({credential:{secretId:cosConfig.SecretId,secretKey:cosConfig.SecretKey},region:"",profile:{httpProfile:{endpoint:"cdn.tencentcloudapi.com"}}});const batchSize=1000;for(let i=0;i<urls.length;i+=batchSize){const batch=urls.slice(i,i+batchSize);await client.PurgeUrlsCache({Urls:batch,Area:area});console.log(`✓ CDN 缓存已刷新 ${batch.length} 个 URL (区域: ${area})`)}}catch(error){if(error.code==="MODULE_NOT_FOUND"){console.warn("\u26A0\uFE0F \u672A\u5B89\u88C5 tencentcloud-sdk-nodejs-cdn\uFF0C\u8DF3\u8FC7 CDN \u5237\u65B0")}else{throw error}}return}// 否则通过服务端 API 代理刷新(支持主/备域名 fallback)
32
+ try{await apiRequest("/aix/playable/api/cdn/purge","POST",{urls,area});console.log(`✓ CDN 缓存已刷新 ${urls.length} 个 URL (区域: ${area})`)}catch(error){throw new Error(`CDN purge failed: ${error.message}`)}}/**
194
33
  * 上传单个文件到 COS
195
34
  *
196
35
  * @param {Object} cosConfig - COS 配置
@@ -198,58 +37,12 @@ async function purgeCDNCache(cosConfig, urls, credentials = null) {
198
37
  * @param {string} cosKey - COS 对象键(远程路径)
199
38
  * @param {Object} credentials - 可选的凭证对象
200
39
  * @returns {Promise<string>} - 上传成功返回访问链接
201
- */
202
- async function uploadFileToCOS(cosConfig, filePath, cosKey, credentials = null) {
203
- if (!fs.existsSync(filePath)) {
204
- throw new Error(`文件不存在: ${filePath}`);
205
- }
206
-
207
- // 获取临时凭证和配置信息
208
- let bucket, region, cdnDomain;
209
-
210
- if (cosConfig && cosConfig.Bucket && cosConfig.Region) {
211
- // 使用配置文件中的信息
212
- bucket = cosConfig.Bucket;
213
- region = cosConfig.Region;
214
- cdnDomain = cosConfig.CdnDomain || 'static.aix.intlgame.com';
215
- } else {
216
- // 从临时凭证 API 获取配置(优先使用传入的凭证,否则重新获取)
217
- if (!credentials) {
218
- credentials = await fetchTemporaryCredentials();
219
- }
220
- bucket = credentials.bucket;
221
- region = credentials.region;
222
- cdnDomain = 'static.aix.intlgame.com';
223
- }
224
-
225
- const cos = await createCOSClient(cosConfig || null, credentials);
226
-
227
- return new Promise((resolve, reject) => {
228
- cos.putObject(
229
- {
230
- Bucket: bucket,
231
- Region: region,
232
- Key: cosKey,
233
- Body: fs.createReadStream(filePath),
234
- },
235
- (err, data) => {
236
- if (err) {
237
- reject(new Error(`上传失败: ${err.message}`));
238
- } else {
239
- // 生成访问链接
240
- // 对路径的每个部分分别进行 URL 编码
241
- const pathParts = cosKey.split('/');
242
- const encodedPath = pathParts.map(part => encodeURIComponent(part)).join('/');
243
-
244
- const url = `https://${cdnDomain}/${encodedPath}`;
245
- resolve(url);
246
- }
247
- }
248
- );
249
- });
250
- }
251
-
252
- /**
40
+ */async function uploadFileToCOS(cosConfig,filePath,cosKey,credentials=null){if(!fs.existsSync(filePath)){throw new Error(`文件不存在: ${filePath}`)}// 获取临时凭证和配置信息
41
+ let bucket,region,cdnDomain;if(cosConfig&&cosConfig.Bucket&&cosConfig.Region){// 使用配置文件中的信息
42
+ bucket=cosConfig.Bucket;region=cosConfig.Region;cdnDomain=cosConfig.CdnDomain||"static.aix.intlgame.com"}else{// 从临时凭证 API 获取配置(优先使用传入的凭证,否则重新获取)
43
+ if(!credentials){credentials=await fetchTemporaryCredentials()}bucket=credentials.bucket;region=credentials.region;cdnDomain="static.aix.intlgame.com"}const cos=await createCOSClient(cosConfig||null,credentials);return new Promise((resolve,reject)=>{cos.putObject({Bucket:bucket,Region:region,Key:cosKey,Body:fs.createReadStream(filePath)},(err,data)=>{if(err){reject(new Error(`上传失败: ${err.message}`))}else{// 生成访问链接
44
+ // 对路径的每个部分分别进行 URL 编码
45
+ const pathParts=cosKey.split("/");const encodedPath=pathParts.map(part=>encodeURIComponent(part)).join("/");const url=`https://${cdnDomain}/${encodedPath}`;resolve(url)}})})}/**
253
46
  * 上传目录下的所有文件到 COS
254
47
  *
255
48
  * @param {Object} cosConfig - COS 配置
@@ -258,67 +51,8 @@ async function uploadFileToCOS(cosConfig, filePath, cosKey, credentials = null)
258
51
  * @param {Object} credentials - 可选的凭证对象
259
52
  * @param {(fileName: string, filePath: string) => boolean} [fileFilter] - 可选文件过滤器,返回 false 则跳过
260
53
  * @returns {Promise<Array>} - 上传成功返回文件列表
261
- */
262
- async function uploadDirectoryToCOS(cosConfig, localDir, cosPrefix, credentials = null, fileFilter = null) {
263
- if (!fs.existsSync(localDir)) {
264
- throw new Error(`目录不存在: ${localDir}`);
265
- }
266
-
267
- // 如果没有传入凭证且没有配置,获取一次凭证用于所有文件上传
268
- if (!credentials && (!cosConfig || !cosConfig.Bucket || !cosConfig.Region)) {
269
- try {
270
- credentials = await fetchTemporaryCredentials();
271
- console.log('✓ 获取临时凭证成功');
272
- } catch (error) {
273
- throw new Error(`获取临时凭证失败: ${error.message}`);
274
- }
275
- }
276
-
277
- const files = fs.readdirSync(localDir);
278
- const results = [];
279
- const uploadedUrls = [];
280
-
281
- for (const file of files) {
282
- const filePath = path.join(localDir, file);
283
- const stat = fs.statSync(filePath);
284
-
285
- if (stat.isFile()) {
286
- if (typeof fileFilter === 'function' && !fileFilter(file, filePath)) {
287
- continue;
288
- }
289
- // 去掉文件名中的 -preview 或 _preview 后缀
290
- let cleanFileName = file;
291
- cleanFileName = cleanFileName.replace(/-preview\./i, '.');
292
- cleanFileName = cleanFileName.replace(/_preview\./i, '.');
293
-
294
- const cosKey = cosPrefix ? `${cosPrefix}/${cleanFileName}` : cleanFileName;
295
- // 传入凭证,避免重复获取
296
- const url = await uploadFileToCOS(cosConfig, filePath, cosKey, credentials);
297
- results.push({
298
- file: cleanFileName,
299
- url,
300
- });
301
- uploadedUrls.push(url);
302
- }
303
- }
304
-
305
- // 上传完成后刷新 CDN 缓存,传入凭证
306
- if (uploadedUrls.length > 0) {
307
- try {
308
- await purgeCDNCache(cosConfig, uploadedUrls, credentials);
309
- console.log(`✓ CDN 缓存刷新成功 (${uploadedUrls.length} 个文件)`);
310
- } catch (error) {
311
- console.warn(`⚠️ CDN 刷新失败: ${error.message}`);
312
- }
313
- }
314
-
315
- return results;
316
- }
317
-
318
- module.exports = {
319
- fetchTemporaryCredentials,
320
- createCOSClient,
321
- uploadFileToCOS,
322
- uploadDirectoryToCOS,
323
- purgeCDNCache,
324
- };
54
+ */async function uploadDirectoryToCOS(cosConfig,localDir,cosPrefix,credentials=null,fileFilter=null){if(!fs.existsSync(localDir)){throw new Error(`目录不存在: ${localDir}`)}// 如果没有传入凭证且没有配置,获取一次凭证用于所有文件上传
55
+ if(!credentials&&(!cosConfig||!cosConfig.Bucket||!cosConfig.Region)){try{credentials=await fetchTemporaryCredentials();console.log("\u2713 \u83B7\u53D6\u4E34\u65F6\u51ED\u8BC1\u6210\u529F")}catch(error){throw new Error(`获取临时凭证失败: ${error.message}`)}}const files=fs.readdirSync(localDir);const results=[];const uploadedUrls=[];for(const file of files){const filePath=path.join(localDir,file);const stat=fs.statSync(filePath);if(stat.isFile()){if(typeof fileFilter==="function"&&!fileFilter(file,filePath)){continue}// 去掉文件名中的 -preview 或 _preview 后缀
56
+ let cleanFileName=file;cleanFileName=cleanFileName.replace(/-preview\./i,".");cleanFileName=cleanFileName.replace(/_preview\./i,".");const cosKey=cosPrefix?`${cosPrefix}/${cleanFileName}`:cleanFileName;// 传入凭证,避免重复获取
57
+ const url=await uploadFileToCOS(cosConfig,filePath,cosKey,credentials);results.push({file:cleanFileName,url});uploadedUrls.push(url)}}// 上传完成后刷新 CDN 缓存,传入凭证
58
+ if(uploadedUrls.length>0){try{await purgeCDNCache(cosConfig,uploadedUrls,credentials);console.log(`✓ CDN 缓存刷新成功 (${uploadedUrls.length} 个文件)`)}catch(error){console.warn(`⚠️ CDN 刷新失败: ${error.message}`)}}return results}module.exports={fetchTemporaryCredentials,createCOSClient,uploadFileToCOS,uploadDirectoryToCOS,purgeCDNCache};
@@ -0,0 +1 @@
1
+ "use strict";const{describe,it}=require("node:test");const assert=require("node:assert/strict");function stripUrl(url){url=String(url||"").trim().split("#")[0].split("?")[0];if(!url)return"";try{if(/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)){const parsed=new URL(url,"https://playable.local/");if(parsed.protocol==="blob:"||parsed.protocol==="data:")return"";url=decodeURIComponent(parsed.pathname||"")}}catch(err){}url=url.replace(/^\/+/,"");const parts=[];url.split("/").forEach(part=>{if(!part||part===".")return;if(part===".."){if(parts.length)parts.pop();return}parts.push(part)});return parts.join("/")}function bestKey(url,target){if(!url||!target)return null;const raw=String(url).split("#")[0].split("?")[0];const stripped=stripUrl(url);if(target[raw])return raw;if(stripped&&target[stripped])return stripped;let best=null;function consider(candidate){if(!candidate||candidate.length<3)return;for(const key in target){if(candidate===key||candidate.endsWith("/"+key)){if(!best||key.length>best.length)best=key}}}consider(stripped);consider(raw);return best}describe("adapter VFS path lookup",()=>{const js={"cocos-js/cc.js":"engine","assets/main/index.js":"game","src/import-map.json":"{\"imports\":{\"cc\":\"./../cocos-js/cc.js\"}}"};it("maps Facebook and Liftoff CDN URLs to the VFS key without changing System ids",()=>{assert.equal(bestKey("https://www.facebook.com/developer/tools/cocos-js/cc.js",js),"cocos-js/cc.js");assert.equal(bestKey("https://cdn.liftoff-creatives.io/tmp/32db3a48-516d-41fb-8508-d1db75caca7e/cocos-js/cc.js",js),"cocos-js/cc.js")});it("maps import-map relative targets after collapsing ..",()=>{assert.equal(bestKey("./../cocos-js/cc.js",js),"cocos-js/cc.js")});it("does not map a short basename onto a different index.js",()=>{assert.equal(bestKey("./dist/index.js",js),null);assert.equal(bestKey("https://example.com/foo/dist/index.js",js),null)});it("resolves the import-map specifier cc to its file target",()=>{const imports=JSON.parse(js["src/import-map.json"]).imports;assert.equal(imports.cc,"./../cocos-js/cc.js");assert.equal(bestKey(imports.cc,js),"cocos-js/cc.js")})});
@@ -1,188 +1,14 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- /** Overlay of /opt/cocos-cli. Not a script cache; recreating it on Linux is expensive. */
7
- const KEEP_TEMP_DIRS = new Set(['cocos-cli']);
8
-
9
- /** Preview merge lives here; wiping it breaks npm run dev until re-bundle (many UUID .json requests). */
10
- const PREVIEW_CACHE_RELS = [
11
- 'temp/programming/preview',
12
- 'temp/programming/packer-driver/targets/preview',
13
- ];
14
-
15
- /** Other temp/* dirs safe to drop before engine build. */
16
- const TEMP_CACHE_DIRS = new Set([
17
- 'asset-db',
18
- 'builder',
19
- 'declarations',
20
- 'logs',
21
- 'writablePath',
22
- ]);
23
-
24
- /** Cocos packer-driver / asset-db recreate these immediately after a clean. */
25
- const WRITABLE_WORKDIR_RELS = [
26
- 'library',
27
- 'temp',
28
- 'temp/programming',
29
- 'temp/programming/packer-driver',
30
- 'temp/programming/packer-driver/logs',
31
- 'temp/builder',
32
- 'build',
33
- ];
34
-
35
- function exists(p) {
36
- try {
37
- return Boolean(p) && fs.existsSync(p);
38
- } catch {
39
- return false;
40
- }
41
- }
42
-
43
- function chmodTreeWritable(dir) {
44
- try {
45
- const st = fs.lstatSync(dir);
46
- if (st.isSymbolicLink()) return;
47
- fs.chmodSync(dir, st.mode | 0o200);
48
- if (!st.isDirectory()) return;
49
- for (const name of fs.readdirSync(dir)) {
50
- chmodTreeWritable(path.join(dir, name));
51
- }
52
- } catch {
53
- // best-effort; rmSync may still fail and we want that error
54
- }
55
- }
56
-
57
- function rmTree(dir) {
58
- if (!exists(dir)) return false;
59
- chmodTreeWritable(dir);
60
- fs.rmSync(dir, { recursive: true, force: true });
61
- return true;
62
- }
63
-
64
- /**
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"]);/** Preview merge lives here; wiping it breaks npm run dev until re-bundle (many UUID .json requests). */const PREVIEW_CACHE_RELS=["temp/programming/preview","temp/programming/packer-driver/targets/preview"];/** Other temp/* dirs safe to drop before engine build. */const TEMP_CACHE_DIRS=new Set(["asset-db","builder","declarations","logs","writablePath"]);/** Cocos packer-driver / asset-db recreate these immediately after a clean. */const WRITABLE_WORKDIR_RELS=["library","temp","temp/programming","temp/programming/packer-driver","temp/programming/packer-driver/logs","temp/builder","build"];function exists(p){try{return Boolean(p)&&fs.existsSync(p)}catch{return false}}function chmodTreeWritable(dir){try{const st=fs.lstatSync(dir);if(st.isSymbolicLink())return;fs.chmodSync(dir,st.mode|128);if(!st.isDirectory())return;for(const name of fs.readdirSync(dir)){chmodTreeWritable(path.join(dir,name))}}catch{// best-effort; rmSync may still fail and we want that error
2
+ }}function rmTree(dir){if(!exists(dir))return false;chmodTreeWritable(dir);fs.rmSync(dir,{recursive:true,force:true});return true}/**
65
3
  * Drop Creator / cocos-cli project caches so packer-driver does not reuse
66
4
  * import-map + chunks that still point at another machine's paths.
67
5
  *
68
6
  * Keeps `temp/cocos-cli` (writable CLI overlay) unless keepCliOverlay is false.
69
7
  *
70
8
  * @returns {string[]} relative paths that were removed
71
- */
72
- function shouldWipePreviewCaches(options = {}, env = process.env) {
73
- if (options.cleanPreviewCache === true) return true;
74
- if (options.cleanPreviewCache === false) return false;
75
- return env.PLAYABLE_CLEAN_PREVIEW_CACHE === '1';
76
- }
77
-
78
- function cleanProgrammingBuildCaches(projectRoot, removed, wipePreview) {
79
- const programming = path.join(projectRoot, 'temp', 'programming');
80
- if (!exists(programming)) return;
81
-
82
- if (wipePreview) {
83
- for (const rel of PREVIEW_CACHE_RELS) {
84
- if (rmTree(path.join(projectRoot, rel))) removed.push(rel);
85
- }
86
- if (rmTree(programming)) {
87
- removed.push('temp/programming');
88
- return;
89
- }
90
- }
91
-
92
- const editor = path.join(programming, 'packer-driver', 'targets', 'editor');
93
- if (rmTree(editor)) removed.push('temp/programming/packer-driver/targets/editor');
94
-
95
- const logs = path.join(programming, 'packer-driver', 'logs');
96
- if (rmTree(logs)) removed.push('temp/programming/packer-driver/logs');
97
- }
98
-
99
- function cleanCocosProjectCaches(projectRoot, options = {}) {
100
- const keepOverlay = options.keepCliOverlay !== false;
101
- const wipePreview = shouldWipePreviewCaches(options, process.env);
102
- const removed = [];
103
-
104
- const library = path.join(projectRoot, 'library');
105
- if (rmTree(library)) removed.push('library');
106
-
107
- const tempRoot = path.join(projectRoot, 'temp');
108
- if (!exists(tempRoot)) return removed;
109
-
110
- if (!keepOverlay) {
111
- if (rmTree(tempRoot)) removed.push('temp');
112
- return removed;
113
- }
114
-
115
- for (const name of fs.readdirSync(tempRoot)) {
116
- if (KEEP_TEMP_DIRS.has(name)) continue;
117
- if (name === 'programming') {
118
- cleanProgrammingBuildCaches(projectRoot, removed, wipePreview);
119
- continue;
120
- }
121
- if (TEMP_CACHE_DIRS.has(name) && rmTree(path.join(tempRoot, name))) {
122
- removed.push(`temp/${name}`);
123
- }
124
- }
125
- return removed;
126
- }
127
-
128
- function ensureDir(dir) {
129
- try {
130
- const st = fs.lstatSync(dir);
131
- if (st.isSymbolicLink() || !st.isDirectory()) {
132
- fs.rmSync(dir, { recursive: true, force: true });
133
- }
134
- } catch (err) {
135
- if (err.code !== 'ENOENT') throw err;
136
- }
137
- fs.mkdirSync(dir, { recursive: true, mode: 0o777 });
138
- try {
139
- fs.chmodSync(dir, 0o777);
140
- } catch {
141
- // chmod may fail on some mounts; mkdir succeeding is enough if we own the dir.
142
- }
143
- }
144
-
145
- /**
9
+ */function shouldWipePreviewCaches(options={},env=process.env){if(options.cleanPreviewCache===true)return true;if(options.cleanPreviewCache===false)return false;return env.PLAYABLE_CLEAN_PREVIEW_CACHE==="1"}function cleanProgrammingBuildCaches(projectRoot,removed,wipePreview){const programming=path.join(projectRoot,"temp","programming");if(!exists(programming))return;if(wipePreview){for(const rel of PREVIEW_CACHE_RELS){if(rmTree(path.join(projectRoot,rel)))removed.push(rel)}if(rmTree(programming)){removed.push("temp/programming");return}}const editor=path.join(programming,"packer-driver","targets","editor");if(rmTree(editor))removed.push("temp/programming/packer-driver/targets/editor");const logs=path.join(programming,"packer-driver","logs");if(rmTree(logs))removed.push("temp/programming/packer-driver/logs")}function cleanCocosProjectCaches(projectRoot,options={}){const keepOverlay=options.keepCliOverlay!==false;const wipePreview=shouldWipePreviewCaches(options,process.env);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(name==="programming"){cleanProgrammingBuildCaches(projectRoot,removed,wipePreview);continue}if(TEMP_CACHE_DIRS.has(name)&&rmTree(path.join(tempRoot,name))){removed.push(`temp/${name}`)}}return removed}function ensureDir(dir){try{const st=fs.lstatSync(dir);if(st.isSymbolicLink()||!st.isDirectory()){fs.rmSync(dir,{recursive:true,force:true})}}catch(err){if(err.code!=="ENOENT")throw err}fs.mkdirSync(dir,{recursive:true,mode:511});try{fs.chmodSync(dir,511)}catch{// chmod may fail on some mounts; mkdir succeeding is enough if we own the dir.
10
+ }}/**
146
11
  * Recreate the dirs cocos-cli writes on startup (winston logs, library).
147
12
  * A leftover read-only `temp/programming` from git / a previous root-owned
148
13
  * overlay makes `mkdir logs` throw EACCES.
149
- */
150
- function ensureWritableCocosWorkdirs(projectRoot) {
151
- const created = [];
152
- for (const rel of WRITABLE_WORKDIR_RELS) {
153
- const dir = path.join(projectRoot, rel);
154
- ensureDir(dir);
155
- created.push(rel);
156
- }
157
- const probe = path.join(projectRoot, 'temp', 'programming', 'packer-driver', 'logs', `.write-probe-${process.pid}`);
158
- try {
159
- fs.writeFileSync(probe, 'ok');
160
- fs.unlinkSync(probe);
161
- } catch (err) {
162
- throw new Error(
163
- `[cocos] ${path.join('temp', 'programming', 'packer-driver', 'logs')} is not writable (${err.code || err.message}). `
164
- + 'In the sandbox image, chown the remix project to the build user and chmod u+rwX temp library build. '
165
- + 'Do not chmod /opt/cocos-cli.'
166
- );
167
- }
168
- return created;
169
- }
170
-
171
- function shouldCleanCocosCache(buildOptions = {}, env = process.env) {
172
- if (env.PLAYABLE_KEEP_COCOS_CACHE === '1') return false;
173
- if (env.PLAYABLE_CLEAN_COCOS_CACHE === '1') return true;
174
- const cocos = buildOptions.cocos || {};
175
- if (cocos.cleanCache === false) return false;
176
- if (cocos.cleanCache === true) return true;
177
- return Boolean(buildOptions.cleanCocosCache);
178
- }
179
-
180
- module.exports = {
181
- KEEP_TEMP_DIRS,
182
- PREVIEW_CACHE_RELS,
183
- WRITABLE_WORKDIR_RELS,
184
- cleanCocosProjectCaches,
185
- ensureWritableCocosWorkdirs,
186
- shouldCleanCocosCache,
187
- shouldWipePreviewCaches,
188
- };
14
+ */function ensureWritableCocosWorkdirs(projectRoot){const created=[];for(const rel of WRITABLE_WORKDIR_RELS){const dir=path.join(projectRoot,rel);ensureDir(dir);created.push(rel)}const probe=path.join(projectRoot,"temp","programming","packer-driver","logs",`.write-probe-${process.pid}`);try{fs.writeFileSync(probe,"ok");fs.unlinkSync(probe)}catch(err){throw new Error(`[cocos] ${path.join("temp","programming","packer-driver","logs")} is not writable (${err.code||err.message}). `+"In the sandbox image, chown the remix project to the build user and chmod u+rwX temp library build. "+"Do not chmod /opt/cocos-cli.")}return created}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,PREVIEW_CACHE_RELS,WRITABLE_WORKDIR_RELS,cleanCocosProjectCaches,ensureWritableCocosWorkdirs,shouldCleanCocosCache,shouldWipePreviewCaches};
@@ -1,75 +1 @@
1
- 'use strict';
2
-
3
- const { describe, it, afterEach } = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const fs = require('fs');
6
- const os = require('os');
7
- const path = require('path');
8
- const { cleanCocosProjectCaches, ensureWritableCocosWorkdirs, shouldCleanCocosCache } = require('./clean-cache.js');
9
-
10
- function writeFile(file, body) {
11
- fs.mkdirSync(path.dirname(file), { recursive: true });
12
- fs.writeFileSync(file, body);
13
- }
14
-
15
- describe('cleanCocosProjectCaches', () => {
16
- let root;
17
-
18
- afterEach(() => {
19
- if (root && fs.existsSync(root)) fs.rmSync(root, { recursive: true, force: true });
20
- });
21
-
22
- it('removes editor build cache but keeps preview merge + cli overlay', () => {
23
- root = fs.mkdtempSync(path.join(os.tmpdir(), 'cocos-clean-cache-'));
24
- writeFile(path.join(root, 'library', 'uuid-to-mtime.json'), '{}');
25
- writeFile(path.join(root, 'temp', 'programming', 'packer-driver', 'targets', 'editor', 'import-map.json'), '{}');
26
- writeFile(path.join(root, 'temp', 'programming', 'packer-driver', 'targets', 'preview', 'chunks', 'playcraft-bundle.js'), 'bundle');
27
- writeFile(path.join(root, 'temp', 'programming', 'preview', 'systemjs', 'playcraft-json-pack.json'), '{}');
28
- writeFile(path.join(root, 'temp', 'builder', 'log.txt'), 'x');
29
- writeFile(path.join(root, 'temp', 'cocos-cli', 'bin', 'cocos'), '#!/usr/bin/env node\n');
30
-
31
- const removed = cleanCocosProjectCaches(root);
32
- assert.deepEqual(
33
- new Set(removed),
34
- new Set(['library', 'temp/programming/packer-driver/targets/editor', 'temp/builder']),
35
- );
36
- assert.equal(fs.existsSync(path.join(root, 'library')), false);
37
- assert.equal(fs.existsSync(path.join(root, 'temp', 'programming', 'packer-driver', 'targets', 'editor')), false);
38
- assert.equal(
39
- fs.existsSync(path.join(root, 'temp', 'programming', 'packer-driver', 'targets', 'preview', 'chunks', 'playcraft-bundle.js')),
40
- true,
41
- );
42
- assert.equal(fs.existsSync(path.join(root, 'temp', 'cocos-cli', 'bin', 'cocos')), true);
43
- });
44
-
45
- it('replaces a leftover programming dir with writable packer logs', () => {
46
- root = fs.mkdtempSync(path.join(os.tmpdir(), 'cocos-clean-cache-'));
47
- writeFile(path.join(root, 'temp', 'programming', 'packer-driver', 'stale.txt'), 'old');
48
- fs.chmodSync(path.join(root, 'temp', 'programming', 'packer-driver'), 0o555);
49
-
50
- cleanCocosProjectCaches(root);
51
- ensureWritableCocosWorkdirs(root);
52
-
53
- const logs = path.join(root, 'temp', 'programming', 'packer-driver', 'logs');
54
- const probe = path.join(logs, 'ok.txt');
55
- fs.writeFileSync(probe, '1');
56
- assert.equal(fs.readFileSync(probe, 'utf8'), '1');
57
- });
58
-
59
- it('can wipe the overlay when keepCliOverlay is false', () => {
60
- root = fs.mkdtempSync(path.join(os.tmpdir(), 'cocos-clean-cache-'));
61
- writeFile(path.join(root, 'temp', 'cocos-cli', 'bin', 'cocos'), 'ok');
62
- const removed = cleanCocosProjectCaches(root, { keepCliOverlay: false });
63
- assert.deepEqual(removed, ['temp']);
64
- assert.equal(fs.existsSync(path.join(root, 'temp')), false);
65
- });
66
- });
67
-
68
- describe('shouldCleanCocosCache', () => {
69
- it('defaults off, env and cocos.cleanCache win', () => {
70
- assert.equal(shouldCleanCocosCache({}, {}), false);
71
- assert.equal(shouldCleanCocosCache({ cocos: { cleanCache: true } }, {}), true);
72
- assert.equal(shouldCleanCocosCache({ cocos: { cleanCache: true } }, { PLAYABLE_KEEP_COCOS_CACHE: '1' }), false);
73
- assert.equal(shouldCleanCocosCache({ cocos: { cleanCache: false } }, { PLAYABLE_CLEAN_COCOS_CACHE: '1' }), true);
74
- });
75
- });
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,ensureWritableCocosWorkdirs,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 editor build cache but keeps preview merge + 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","programming","packer-driver","targets","preview","chunks","playcraft-bundle.js"),"bundle");writeFile(path.join(root,"temp","programming","preview","systemjs","playcraft-json-pack.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/packer-driver/targets/editor","temp/builder"]));assert.equal(fs.existsSync(path.join(root,"library")),false);assert.equal(fs.existsSync(path.join(root,"temp","programming","packer-driver","targets","editor")),false);assert.equal(fs.existsSync(path.join(root,"temp","programming","packer-driver","targets","preview","chunks","playcraft-bundle.js")),true);assert.equal(fs.existsSync(path.join(root,"temp","cocos-cli","bin","cocos")),true)});it("replaces a leftover programming dir with writable packer logs",()=>{root=fs.mkdtempSync(path.join(os.tmpdir(),"cocos-clean-cache-"));writeFile(path.join(root,"temp","programming","packer-driver","stale.txt"),"old");fs.chmodSync(path.join(root,"temp","programming","packer-driver"),365);cleanCocosProjectCaches(root);ensureWritableCocosWorkdirs(root);const logs=path.join(root,"temp","programming","packer-driver","logs");const probe=path.join(logs,"ok.txt");fs.writeFileSync(probe,"1");assert.equal(fs.readFileSync(probe,"utf8"),"1")});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)})});