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

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 (95) 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/clean-cache.js +5 -179
  15. package/core/engines/cocos/clean-cache.test.js +1 -75
  16. package/core/engines/cocos/cli-build.js +2 -116
  17. package/core/engines/cocos/cli.js +11 -38
  18. package/core/engines/cocos/constants.js +1 -21
  19. package/core/engines/cocos/creator-cli.js +1 -108
  20. package/core/engines/cocos/dev-server.js +4 -412
  21. package/core/engines/cocos/fs.js +1 -105
  22. package/core/engines/cocos/injects/3x/init.js +19 -438
  23. package/core/engines/cocos/injects/3x/main.js +2 -52
  24. package/core/engines/cocos/injects/libs/pako.js +1 -2
  25. package/core/engines/cocos/mcp-refresh.js +2 -76
  26. package/core/engines/cocos/merge.js +3 -148
  27. package/core/engines/cocos/pack.js +1 -187
  28. package/core/engines/cocos/preview-cache.js +2 -48
  29. package/core/engines/cocos/preview-cache.test.js +1 -32
  30. package/core/engines/cocos/preview-concat.js +5 -189
  31. package/core/engines/cocos/preview-concat.test.js +1 -73
  32. package/core/engines/cocos/preview-json-hook.js +1 -200
  33. package/core/engines/cocos/preview-json-pack.js +3 -376
  34. package/core/engines/cocos/preview-json-pack.test.js +2 -226
  35. package/core/engines/cocos/preview-proxy.js +1 -132
  36. package/core/engines/cocos/rebundle-preview.js +2 -54
  37. package/core/engines/cocos/strip-social-share.js +3 -115
  38. package/core/engines/cocos/theme-assets.js +2 -161
  39. package/core/engines/cocos/writable-cli.js +2 -248
  40. package/core/engines/cocos/writable-cli.test.js +1 -112
  41. package/core/engines/web/index.js +1 -20
  42. package/core/engines/web/vite.dev.js +17 -322
  43. package/core/engines/web/webpack.build.js +37 -522
  44. package/core/engines/web/webpack.common.js +11 -177
  45. package/core/engines/web/webpack.dev.js +6 -88
  46. package/core/index.js +7 -101
  47. package/core/injectors/apply.js +1 -94
  48. package/core/injectors/check-channel-matrix.js +2 -130
  49. package/core/injectors/spec.js +9 -217
  50. package/core/loaders/gltf-loader.js +28 -74
  51. package/core/options.js +16 -353
  52. package/core/plugins/AdikteevInjectorPlugin.js +4 -26
  53. package/core/plugins/BigoAdsInjectorPlugin.js +4 -21
  54. package/core/plugins/DAPIInjectorPlugin.js +2 -25
  55. package/core/plugins/DebuggerInjectionPlugin.js +3 -31
  56. package/core/plugins/ExitAPIInjectorPlugin.js +3 -56
  57. package/core/plugins/FflateCompressionPlugin.js +40 -231
  58. package/core/plugins/FontInjectionWebpackPlugin.js +4 -46
  59. package/core/plugins/LiftoffInjectorPlugin.js +4 -26
  60. package/core/plugins/MRAIDInjectorPlugin.js +2 -24
  61. package/core/plugins/MintegralInjectorPlugin.js +2 -25
  62. package/core/plugins/PangleInjectorPlugin.js +2 -24
  63. package/core/plugins/PlaycraftOrientationInjectorPlugin.js +3 -29
  64. package/core/plugins/SnapchatInjectorPlugin.js +4 -26
  65. package/core/plugins/SocialMediaInjectorPlugin.js +7 -119
  66. package/core/plugins/TikTokInjectorPlugin.js +2 -24
  67. package/core/plugins/UnityInjectorPlugin.js +8 -37
  68. package/core/plugins/ZipPlugin.js +20 -154
  69. package/core/plugins/socialMediaVitePlugin.js +2 -108
  70. package/core/utils/buildDefines.js +2 -30
  71. package/core/utils/buildTemplateString.js +5 -39
  72. package/core/utils/date.js +1 -16
  73. package/core/utils/fontRegistry.js +13 -81
  74. package/core/utils/fontResolver.js +37 -335
  75. package/core/utils/generateAdikteevHtmlWebpackPluginConfig.js +7 -42
  76. package/core/utils/generateBigabidHtmlWebpackPluginConfig.js +3 -30
  77. package/core/utils/generateInMobiHtmlWebpackPluginConfig.js +7 -43
  78. package/core/utils/getPlaycraftOrientationSuffix.js +2 -9
  79. package/core/utils/injectSDKPlugins.js +13 -68
  80. package/core/utils/logOptions.js +5 -46
  81. package/core/utils/mergeOptions.js +2 -21
  82. package/core/utils/obfuscateLevels.js +10 -0
  83. package/core/utils/obfuscateLevels.test.js +10 -0
  84. package/core/utils/parseArgvOptions.js +5 -125
  85. package/core/utils/resolveChannelFold2Zip.js +3 -15
  86. package/core/utils/validateThemeData.js +26 -227
  87. package/core/validators/pre-build-checker.js +25 -287
  88. package/core/validators/tracking-validator.js +40 -441
  89. package/core/validators/tracking-validator.test.js +2 -84
  90. package/core/vite.dev.js +1 -1
  91. package/core/webpack.build.js +1 -1
  92. package/core/webpack.common.js +1 -1
  93. package/core/webpack.dev.js +1 -1
  94. package/package.json +2 -3
  95. 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};
@@ -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)})});