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

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 (94) hide show
  1. package/README.md +262 -0
  2. package/cli/bin/playable-scripts.js +53 -6
  3. package/cli/commands/build.js +241 -32
  4. package/cli/commands/builds.js +13 -2
  5. package/cli/commands/dev.js +22 -1
  6. package/cli/commands/pack.js +323 -41
  7. package/cli/commands/vite-dev.js +7 -1
  8. package/core/artifacts/emit.js +98 -1
  9. package/core/artifacts/fflate.js +31 -1
  10. package/core/artifacts/zip.js +39 -1
  11. package/core/batch-build.js +1261 -149
  12. package/core/constants.js +20 -2
  13. package/core/cos-uploader.js +289 -23
  14. package/core/engines/cocos/clean-cache.js +188 -0
  15. package/core/engines/cocos/clean-cache.test.js +75 -0
  16. package/core/engines/cocos/cli-build.js +116 -2
  17. package/core/engines/cocos/cli.js +38 -1
  18. package/core/engines/cocos/constants.js +21 -1
  19. package/core/engines/cocos/creator-cli.js +108 -1
  20. package/core/engines/cocos/dev-server.js +412 -2
  21. package/core/engines/cocos/fs.js +105 -1
  22. package/core/engines/cocos/injects/3x/init.js +438 -19
  23. package/core/engines/cocos/injects/3x/main.js +52 -2
  24. package/core/engines/cocos/injects/libs/pako.js +2 -1
  25. package/core/engines/cocos/mcp-refresh.js +76 -2
  26. package/core/engines/cocos/merge.js +147 -2
  27. package/core/engines/cocos/pack.js +187 -1
  28. package/core/engines/cocos/preview-cache.js +51 -0
  29. package/core/engines/cocos/preview-cache.test.js +32 -0
  30. package/core/engines/cocos/preview-concat.js +192 -0
  31. package/core/engines/cocos/preview-concat.test.js +73 -0
  32. package/core/engines/cocos/preview-json-hook.js +200 -0
  33. package/core/engines/cocos/preview-json-pack.js +379 -0
  34. package/core/engines/cocos/preview-json-pack.test.js +226 -0
  35. package/core/engines/cocos/preview-proxy.js +132 -0
  36. package/core/engines/cocos/rebundle-preview.js +56 -0
  37. package/core/engines/cocos/strip-social-share.js +115 -3
  38. package/core/engines/cocos/theme-assets.js +161 -2
  39. package/core/engines/cocos/writable-cli.js +250 -0
  40. package/core/engines/cocos/writable-cli.test.js +112 -0
  41. package/core/engines/web/index.js +20 -1
  42. package/core/engines/web/vite.dev.js +322 -17
  43. package/core/engines/web/webpack.build.js +522 -65
  44. package/core/engines/web/webpack.common.js +177 -11
  45. package/core/engines/web/webpack.dev.js +88 -6
  46. package/core/index.d.ts +3 -0
  47. package/core/index.js +101 -7
  48. package/core/injectors/apply.js +94 -1
  49. package/core/injectors/check-channel-matrix.js +130 -2
  50. package/core/injectors/spec.js +217 -9
  51. package/core/loaders/gltf-loader.js +74 -28
  52. package/core/options.js +353 -16
  53. package/core/plugins/AdikteevInjectorPlugin.js +26 -4
  54. package/core/plugins/BigoAdsInjectorPlugin.js +21 -4
  55. package/core/plugins/DAPIInjectorPlugin.js +25 -2
  56. package/core/plugins/DebuggerInjectionPlugin.js +31 -3
  57. package/core/plugins/ExitAPIInjectorPlugin.js +56 -3
  58. package/core/plugins/FflateCompressionPlugin.js +231 -40
  59. package/core/plugins/FontInjectionWebpackPlugin.js +46 -4
  60. package/core/plugins/LiftoffInjectorPlugin.js +26 -4
  61. package/core/plugins/MRAIDInjectorPlugin.js +24 -2
  62. package/core/plugins/MintegralInjectorPlugin.js +25 -2
  63. package/core/plugins/PangleInjectorPlugin.js +24 -2
  64. package/core/plugins/PlaycraftOrientationInjectorPlugin.js +29 -3
  65. package/core/plugins/SnapchatInjectorPlugin.js +26 -4
  66. package/core/plugins/SocialMediaInjectorPlugin.js +119 -7
  67. package/core/plugins/TikTokInjectorPlugin.js +24 -2
  68. package/core/plugins/UnityInjectorPlugin.js +37 -8
  69. package/core/plugins/ZipPlugin.js +154 -20
  70. package/core/plugins/socialMediaVitePlugin.js +108 -2
  71. package/core/utils/buildDefines.js +30 -2
  72. package/core/utils/buildTemplateString.js +39 -5
  73. package/core/utils/date.js +16 -1
  74. package/core/utils/fontRegistry.js +81 -13
  75. package/core/utils/fontResolver.js +335 -37
  76. package/core/utils/generateAdikteevHtmlWebpackPluginConfig.js +42 -7
  77. package/core/utils/generateBigabidHtmlWebpackPluginConfig.js +30 -3
  78. package/core/utils/generateInMobiHtmlWebpackPluginConfig.js +43 -7
  79. package/core/utils/getPlaycraftOrientationSuffix.js +9 -2
  80. package/core/utils/injectSDKPlugins.js +68 -13
  81. package/core/utils/logOptions.js +46 -5
  82. package/core/utils/mergeOptions.js +21 -2
  83. package/core/utils/parseArgvOptions.js +125 -5
  84. package/core/utils/resolveChannelFold2Zip.js +15 -3
  85. package/core/utils/validateThemeData.js +227 -26
  86. package/core/validators/index.d.ts +4 -0
  87. package/core/validators/pre-build-checker.js +287 -25
  88. package/core/validators/tracking-validator.js +444 -43
  89. package/core/validators/tracking-validator.test.js +92 -0
  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 +3 -2
@@ -1,6 +1,10 @@
1
- "use strict";const fs=require("fs");const path=require("path");// ============================================================
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ // ============================================================
2
5
  // 埋点方法配置
3
6
  // ============================================================
7
+
4
8
  /**
5
9
  * 埋点方法配置
6
10
  * @typedef {Object} TrackingMethod
@@ -8,30 +12,212 @@
8
12
  * @property {boolean} required - 是否必须调用
9
13
  * @property {string} description - 方法描述
10
14
  * @property {string[]} channels - 对应的渠道埋点
11
- *//**
15
+ */
16
+
17
+ /**
12
18
  * 所有埋点方法配置
13
19
  * @type {TrackingMethod[]}
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"]}];/**
20
+ */
21
+ const TRACKING_METHODS = [
22
+ {
23
+ name: 'onPlaycraftTrackingInit',
24
+ required: true,
25
+ description: 'SDK 初始化开始',
26
+ channels: ['Bigabid:mraid_viewable', 'InMobi:Ad_Load_Start', 'AppLovin:LOADING']
27
+ },
28
+ {
29
+ name: 'onPlaycraftTrackingLoading',
30
+ required: true,
31
+ description: '游戏资源加载中',
32
+ channels: ['Bigabid:game_viewable', 'InMobi:Ad_Viewable', 'AppLovin:LOADED']
33
+ },
34
+ {
35
+ name: 'onPlaycraftTrackingLoaded',
36
+ required: true,
37
+ description: '主场景就绪,用户可交互',
38
+ channels: ['AppLovin:DISPLAYED']
39
+ },
40
+ {
41
+ name: 'onPlaycraftChallengeStart',
42
+ required: true,
43
+ description: '挑战开始(首次用户交互)',
44
+ channels: ['Bigabid:engagement', 'InMobi:First_Engagement', 'AppLovin:CHALLENGE_STARTED']
45
+ },
46
+ {
47
+ name: 'onPlaycraftChallengeProgress',
48
+ required: false,
49
+ description: '进度百分比追踪(可选)',
50
+ channels: ['AppLovin:CHALLENGE_PASS_*']
51
+ },
52
+ {
53
+ name: 'onPlaycraftChallengeSuccess',
54
+ required: true,
55
+ description: '游戏挑战成功',
56
+ channels: ['AppLovin:CHALLENGE_SOLVED', 'Bigabid:complete', 'InMobi:Gameplay_Complete']
57
+ },
58
+ {
59
+ name: 'onPlaycraftChallengeFinish',
60
+ required: false,
61
+ description: '游戏结束(可选)',
62
+ channels: ['Bigabid:complete', 'InMobi:Gameplay_Complete', 'AppLovin:ENDCARD_SHOWN']
63
+ },
64
+ {
65
+ name: 'onPlaycraftInstall',
66
+ required: true,
67
+ description: '点击安装',
68
+ channels: ['Bigabid:click', 'InMobi:DSP_Click', 'AppLovin:CTA_CLICKED']
69
+ },
70
+ {
71
+ name: 'onPlaycraftRetry',
72
+ required: false,
73
+ description: '重试游戏(可选)',
74
+ channels: ['AppLovin:CHALLENGE_RETRY']
75
+ },
76
+ {
77
+ name: 'onPlaycraftChallengeFailed',
78
+ required: true,
79
+ description: '游戏挑战失败',
80
+ channels: ['AppLovin:CHALLENGE_FAILED']
81
+ }
82
+ ];
83
+
84
+ /**
15
85
  * SDK 静态方法映射到 tracking 方法
16
86
  * 这些 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"};// ============================================================
87
+ */
88
+ const SDK_TO_TRACKING_MAP = {
89
+ 'playcraftInit': 'onPlaycraftTrackingInit',
90
+ 'playcraftStart': 'onPlaycraftTrackingLoading',
91
+ 'playcraftInteractive': 'onPlaycraftTrackingLoaded',
92
+ 'playcraftChallengeStart': 'onPlaycraftChallengeStart',
93
+ 'recordPlaycraftChallengeProgress': 'onPlaycraftChallengeProgress',
94
+ 'playcraftRecordSuccess': 'onPlaycraftChallengeSuccess',
95
+ 'playcraftFinish': 'onPlaycraftChallengeFinish',
96
+ 'playcraftInstall': 'onPlaycraftInstall',
97
+ 'playcraftRetry': 'onPlaycraftRetry',
98
+ 'playcraftRecordFailed': 'onPlaycraftChallengeFailed'
99
+ };
100
+
101
+ /** Phaser / Three 游戏逻辑在 src;Cocos 在 assets/scripts。不要扫 library/temp。 */
102
+ const ENGINE_SCAN_ROOTS = {
103
+ cocos: ['assets/scripts'],
104
+ phaser: ['src'],
105
+ threejs: ['src'],
106
+ };
107
+
108
+ const DEFAULT_EXCLUDE_DIRS = [
109
+ 'node_modules', 'dist', 'build', '.git', '.claude',
110
+ 'library', 'temp', 'profiles', 'native', 'local', 'dist-all',
111
+ ];
112
+
113
+ function detectProjectEngine(projectRoot, explicit) {
114
+ if (explicit) return String(explicit).toLowerCase();
115
+ try {
116
+ const configPath = path.join(projectRoot, 'builds.config.js');
117
+ if (fs.existsSync(configPath)) {
118
+ delete require.cache[require.resolve(configPath)];
119
+ const cfg = require(configPath);
120
+ if (cfg && cfg.engine) return String(cfg.engine).toLowerCase();
121
+ }
122
+ } catch {
123
+ // ignore invalid builds.config.js
124
+ }
125
+ if (fs.existsSync(path.join(projectRoot, 'settings/cocos.config.json'))) return 'cocos';
126
+ if (
127
+ fs.existsSync(path.join(projectRoot, 'assets/scripts')) &&
128
+ fs.existsSync(path.join(projectRoot, 'assets/scenes'))
129
+ ) {
130
+ return 'cocos';
131
+ }
132
+ return '';
133
+ }
134
+
135
+ function resolveScanRoots(projectRoot, engine) {
136
+ const rels = ENGINE_SCAN_ROOTS[engine] || ENGINE_SCAN_ROOTS.phaser;
137
+ const roots = rels
138
+ .map((rel) => path.join(projectRoot, rel))
139
+ .filter((dir) => fs.existsSync(dir));
140
+ return roots.length ? roots : [projectRoot];
141
+ }
142
+
143
+ function toRelScanRoots(projectRoot, roots) {
144
+ return roots.map((dir) => {
145
+ const rel = path.relative(projectRoot, dir);
146
+ return rel || '.';
147
+ });
148
+ }
149
+
150
+ // ============================================================
18
151
  // 文件扫描工具
19
152
  // ============================================================
153
+
20
154
  /**
21
155
  * 递归扫描目录获取所有源码文件
22
156
  * @param {string} dir - 目录路径
23
157
  * @param {string[]} extensions - 文件扩展名列表
24
158
  * @param {string[]} excludeDirs - 排除的目录名列表
25
159
  * @returns {string[]} 文件路径列表
26
- */function scanSourceFiles(dir,extensions=[".ts",".tsx",".js",".jsx"],excludeDirs=["node_modules","dist","build",".git",".claude"]){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
- 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}/**
160
+ */
161
+ function scanSourceFiles(dir, extensions = ['.ts', '.tsx', '.js', '.jsx'], excludeDirs = DEFAULT_EXCLUDE_DIRS) {
162
+ const files = [];
163
+
164
+ function scan(currentDir) {
165
+ if (!fs.existsSync(currentDir)) return;
166
+
167
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
168
+
169
+ for (const entry of entries) {
170
+ const fullPath = path.join(currentDir, entry.name);
171
+
172
+ if (entry.isDirectory()) {
173
+ // 跳过排除的目录
174
+ if (excludeDirs.includes(entry.name)) continue;
175
+ scan(fullPath);
176
+ } else if (entry.isFile()) {
177
+ // 检查文件扩展名
178
+ const ext = path.extname(entry.name);
179
+ if (extensions.includes(ext)) {
180
+ files.push(fullPath);
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ scan(dir);
187
+ return files;
188
+ }
189
+
190
+ function scanSourceFilesFromRoots(roots, extensions, excludeDirs) {
191
+ const files = [];
192
+ const seen = new Set();
193
+ for (const root of roots) {
194
+ for (const file of scanSourceFiles(root, extensions, excludeDirs)) {
195
+ if (seen.has(file)) continue;
196
+ seen.add(file);
197
+ files.push(file);
198
+ }
199
+ }
200
+ return files;
201
+ }
202
+
203
+ /**
29
204
  * 读取文件内容(带错误处理)
30
205
  * @param {string} filePath - 文件路径
31
206
  * @returns {string|null} 文件内容或 null
32
- */function readFileContent(filePath){try{return fs.readFileSync(filePath,"utf-8")}catch(err){console.warn(`[警告] 无法读取文件: ${filePath}`,err.message);return null}}// ============================================================
207
+ */
208
+ function readFileContent(filePath) {
209
+ try {
210
+ return fs.readFileSync(filePath, 'utf-8');
211
+ } catch (err) {
212
+ console.warn(`[警告] 无法读取文件: ${filePath}`, err.message);
213
+ return null;
214
+ }
215
+ }
216
+
217
+ // ============================================================
33
218
  // 方法调用检测
34
219
  // ============================================================
220
+
35
221
  /**
36
222
  * 检测代码中是否包含 tracking 方法的直接调用
37
223
  * @param {string} content - 文件内容
@@ -40,10 +226,21 @@ const ext=path.extname(entry.name);if(extensions.includes(ext)){files.push(fullP
40
226
  *
41
227
  * 注意:不依赖对象名称,只检测方法调用模式
42
228
  * 匹配:xxx.onPlaycraftXxx( 或 xxx['onPlaycraftXxx']( 或 xxx["onPlaycraftXxx"](
43
- */function detectDirectTrackingCall(content,methodName){const patterns=[// 匹配 .方法名(
44
- new RegExp(`\\.${methodName}\\s*\\(`),// 匹配 ['方法名'](
45
- new RegExp(`\\['${methodName}'\\]\\s*\\(`),// 匹配 ["方法名"](
46
- new RegExp(`\\["${methodName}"\\]\\s*\\(`)];return patterns.some(pattern=>pattern.test(content))}/**
229
+ */
230
+ function detectDirectTrackingCall(content, methodName) {
231
+ const patterns = [
232
+ // 匹配 .方法名(
233
+ new RegExp(`\\.${methodName}\\s*\\(`),
234
+ // 匹配 ['方法名'](
235
+ new RegExp(`\\['${methodName}'\\]\\s*\\(`),
236
+ // 匹配 ["方法名"](
237
+ new RegExp(`\\["${methodName}"\\]\\s*\\(`)
238
+ ];
239
+
240
+ return patterns.some(pattern => pattern.test(content));
241
+ }
242
+
243
+ /**
47
244
  * 检测代码中是否包含 SDK 方法调用(会间接触发 tracking)
48
245
  * @param {string} content - 文件内容
49
246
  * @param {string} sdkMethod - SDK 方法名
@@ -51,20 +248,48 @@ new RegExp(`\\["${methodName}"\\]\\s*\\(`)];return patterns.some(pattern=>patter
51
248
  *
52
249
  * 注意:不依赖对象名称,只检测方法调用模式
53
250
  * 匹配:xxx.playcraftXxx( 或 xxx['playcraftXxx']( 或 xxx["playcraftXxx"](
54
- */function detectSDKCall(content,sdkMethod){const patterns=[// 匹配 .方法名(
55
- new RegExp(`\\.${sdkMethod}\\s*\\(`),// 匹配 ['方法名'](
56
- new RegExp(`\\['${sdkMethod}'\\]\\s*\\(`),// 匹配 ["方法名"](
57
- new RegExp(`\\["${sdkMethod}"\\]\\s*\\(`)];return patterns.some(pattern=>pattern.test(content))}/**
251
+ */
252
+ function detectSDKCall(content, sdkMethod) {
253
+ const patterns = [
254
+ // 匹配 .方法名(
255
+ new RegExp(`\\.${sdkMethod}\\s*\\(`),
256
+ // 匹配 ['方法名'](
257
+ new RegExp(`\\['${sdkMethod}'\\]\\s*\\(`),
258
+ // 匹配 ["方法名"](
259
+ new RegExp(`\\["${sdkMethod}"\\]\\s*\\(`)
260
+ ];
261
+
262
+ return patterns.some(pattern => pattern.test(content));
263
+ }
264
+
265
+ /**
58
266
  * 检测是否导入了 @playcraft/ads-tracking
59
267
  * @param {string} content - 文件内容
60
268
  * @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
- * @param {string} content - 文件内容
64
- * @returns {boolean} 是否导入了 SDK
65
- */function hasSDKImport(content){return /from\s+['"]@playcraft\/adsdk['"]/.test(content)||/require\s*\(\s*['"]@playcraft\/adsdk['"]\s*\)/.test(content)}// ============================================================
269
+ */
270
+ function hasTrackingImport(content) {
271
+ return /from\s+['"]@playcraft\/ads-tracking(?:\/[^'"]*)?['"]/.test(content) ||
272
+ /require\s*\(\s*['"]@playcraft\/ads-tracking(?:\/[^'"]*)?['"]\s*\)/.test(content);
273
+ }
274
+
275
+ /**
276
+ * 检测是否导入了 @playcraft/adsdk(含 /cjs 等子路径,以及项目内 playcraft-adsdk 封装)
277
+ */
278
+ function hasSDKImport(content) {
279
+ return /from\s+['"]@playcraft\/adsdk(?:\/[^'"]*)?['"]/.test(content) ||
280
+ /require\s*\(\s*['"]@playcraft\/adsdk(?:\/[^'"]*)?['"]\s*\)/.test(content) ||
281
+ /from\s+['"][^'"]*playcraft-adsdk['"]/.test(content) ||
282
+ /require\s*\(\s*['"][^'"]*playcraft-adsdk['"]\s*\)/.test(content);
283
+ }
284
+
285
+ function hasAnySdkTrackingCall(content) {
286
+ return Object.keys(SDK_TO_TRACKING_MAP).some((sdkMethod) => detectSDKCall(content, sdkMethod));
287
+ }
288
+
289
+ // ============================================================
66
290
  // 验证核心逻辑
67
291
  // ============================================================
292
+
68
293
  /**
69
294
  * 验证结果
70
295
  * @typedef {Object} ValidationResult
@@ -74,40 +299,216 @@ new RegExp(`\\["${sdkMethod}"\\]\\s*\\(`)];return patterns.some(pattern=>pattern
74
299
  * @property {string[]} errors - 错误信息列表
75
300
  * @property {string[]} warnings - 警告信息列表
76
301
  * @property {Object} stats - 统计信息
77
- *//**
302
+ */
303
+
304
+ /**
78
305
  * 验证项目中的埋点调用
79
306
  * @param {string} projectRoot - 项目根目录
80
307
  * @param {Object} [options] - 验证选项
81
308
  * @param {string[]} [options.extensions] - 文件扩展名
82
309
  * @param {string[]} [options.excludeDirs] - 排除的目录
310
+ * @param {string} [options.engine] - 引擎:cocos / phaser / threejs
311
+ * @param {string[]} [options.scanDirs] - 覆盖默认扫描目录(相对项目根)
83
312
  * @param {boolean} [options.verbose] - 是否输出详细信息
84
313
  * @returns {ValidationResult} 验证结果
85
- */function validateTracking(projectRoot,options={}){const{extensions=[".ts",".tsx",".js",".jsx"],excludeDirs=["node_modules","dist","build",".git"],verbose=false}=options;const result={success:false,integrationType:"none",methods:[],errors:[],warnings:[],stats:{totalFiles:0,scannedFiles:0,hasTrackingImport:false,hasSDKImport:false}};// 1. 扫描所有源码文件
86
- if(verbose)console.log(`\n[扫描] 开始扫描项目: ${projectRoot}`);const files=scanSourceFiles(projectRoot,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
- 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.hasTrackingImport&&result.stats.hasSDKImport){result.integrationType="sdk";// SDK 优先
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. 检测每个埋点方法
90
- for(const method of TRACKING_METHODS){let found=false;if(result.integrationType==="sdk"){// SDK 集成:检测 SDK 方法调用
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
- found=detectDirectTrackingCall(allContent,method.name)}result.methods.push({method:method.name,found,required:method.required,description:method.description});// 必须方法未找到 -> 错误
93
- if(method.required&&!found){result.errors.push(`❌ 缺少必须的埋点方法: ${method.name} (${method.description})`)}// 可选方法未找到 -> 警告
94
- if(!method.required&&!found){result.warnings.push(`⚠️ 未找到可选埋点方法: ${method.name} (${method.description})`)}}// 5. 判断验证结果
95
- result.success=result.errors.length===0;return result}// ============================================================
314
+ */
315
+ function validateTracking(projectRoot, options = {}) {
316
+ const {
317
+ extensions = ['.ts', '.tsx', '.js', '.jsx'],
318
+ excludeDirs = DEFAULT_EXCLUDE_DIRS,
319
+ verbose = false
320
+ } = options;
321
+ const engine = detectProjectEngine(projectRoot, options.engine);
322
+ const scanRoots = Array.isArray(options.scanDirs) && options.scanDirs.length
323
+ ? options.scanDirs.map((dir) => path.resolve(projectRoot, dir)).filter((dir) => fs.existsSync(dir))
324
+ : resolveScanRoots(projectRoot, engine);
325
+
326
+ const result = {
327
+ success: false,
328
+ integrationType: 'none',
329
+ methods: [],
330
+ errors: [],
331
+ warnings: [],
332
+ stats: {
333
+ totalFiles: 0,
334
+ scannedFiles: 0,
335
+ hasTrackingImport: false,
336
+ hasSDKImport: false,
337
+ engine,
338
+ scanRoots: toRelScanRoots(projectRoot, scanRoots),
339
+ }
340
+ };
341
+
342
+ // 1. 按引擎扫描源码目录,避免 Cocos library/temp 把检测带偏
343
+ if (verbose) {
344
+ console.log(`\n[扫描] 项目: ${projectRoot}`);
345
+ console.log(`[扫描] 引擎: ${engine || '(default/src)'}`);
346
+ console.log(`[扫描] 目录: ${result.stats.scanRoots.join(', ')}`);
347
+ }
348
+ const files = scanSourceFilesFromRoots(scanRoots, extensions, excludeDirs);
349
+ result.stats.totalFiles = files.length;
350
+
351
+ if (files.length === 0) {
352
+ result.errors.push('未找到任何源码文件');
353
+ return result;
354
+ }
355
+
356
+ if (verbose) console.log(`[扫描] 找到 ${files.length} 个源码文件\n`);
357
+
358
+ // 2. 读取所有文件内容并合并(用于全局检测)
359
+ let allContent = '';
360
+ for (const file of files) {
361
+ const content = readFileContent(file);
362
+ if (content) {
363
+ allContent += content + '\n';
364
+ result.stats.scannedFiles++;
365
+ }
366
+ }
367
+
368
+ // 3. 判断集成类型
369
+ result.stats.hasTrackingImport = hasTrackingImport(allContent);
370
+ result.stats.hasSDKImport = hasSDKImport(allContent);
371
+
372
+ if (result.stats.hasSDKImport || hasAnySdkTrackingCall(allContent)) {
373
+ result.integrationType = 'sdk';
374
+ } else if (result.stats.hasTrackingImport) {
375
+ result.integrationType = 'tracking';
376
+ } else {
377
+ result.errors.push('未检测到 @playcraft/adsdk 或 @playcraft/ads-tracking 的导入');
378
+ return result;
379
+ }
380
+
381
+ if (verbose) {
382
+ console.log(`[集成类型] ${result.integrationType === 'sdk' ? 'SDK 集成(通过 sdk 静态方法调用)' : '直接 tracking 集成'}`);
383
+ console.log(`[导入检测] SDK: ${result.stats.hasSDKImport ? '✓' : '✗'} | Tracking: ${result.stats.hasTrackingImport ? '✓' : '✗'}\n`);
384
+ }
385
+
386
+ // 4. 检测每个埋点方法
387
+ for (const method of TRACKING_METHODS) {
388
+ let found = false;
389
+
390
+ if (result.integrationType === 'sdk') {
391
+ // SDK 集成:检测 SDK 方法调用
392
+ const sdkMethod = Object.keys(SDK_TO_TRACKING_MAP).find(
393
+ key => SDK_TO_TRACKING_MAP[key] === method.name
394
+ );
395
+ if (sdkMethod) {
396
+ found = detectSDKCall(allContent, sdkMethod);
397
+ }
398
+ } else if (result.integrationType === 'tracking') {
399
+ // 直接 tracking 集成:检测 tracking 方法调用
400
+ found = detectDirectTrackingCall(allContent, method.name);
401
+ }
402
+
403
+ result.methods.push({
404
+ method: method.name,
405
+ found,
406
+ required: method.required,
407
+ description: method.description
408
+ });
409
+
410
+ // 必须方法未找到 -> 错误
411
+ if (method.required && !found) {
412
+ result.errors.push(`❌ 缺少必须的埋点方法: ${method.name} (${method.description})`);
413
+ }
414
+
415
+ // 可选方法未找到 -> 警告
416
+ if (!method.required && !found) {
417
+ result.warnings.push(`⚠️ 未找到可选埋点方法: ${method.name} (${method.description})`);
418
+ }
419
+ }
420
+
421
+ // 5. 判断验证结果
422
+ result.success = result.errors.length === 0;
423
+
424
+ return result;
425
+ }
426
+
427
+ // ============================================================
96
428
  // 格式化输出
97
429
  // ============================================================
430
+
98
431
  /**
99
432
  * 格式化验证结果并输出到控制台
100
433
  * @param {ValidationResult} result - 验证结果
101
434
  * @param {boolean} [showDetails=true] - 是否显示详细信息
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"}`);// 方法检测结果
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
- if(result.errors.length>0){console.log("\n\u274C \u9519\u8BEF:");result.errors.forEach(err=>console.log(` ${err}`))}// 警告信息
106
- if(result.warnings.length>0){console.log("\n\u26A0\uFE0F \u8B66\u544A:");result.warnings.forEach(warn=>console.log(` ${warn}`))}// 最终结果
107
- console.log("\n"+"=".repeat(60));if(result.success){console.log("\u2705 \u9A8C\u8BC1\u901A\u8FC7\uFF01\u6240\u6709\u5FC5\u987B\u7684\u57CB\u70B9\u65B9\u6CD5\u90FD\u5DF2\u6B63\u786E\u8C03\u7528\u3002")}else{console.log("\u274C \u9A8C\u8BC1\u5931\u8D25\uFF01\u5B58\u5728\u7F3A\u5931\u7684\u5FC5\u987B\u57CB\u70B9\u65B9\u6CD5\uFF0C\u8BF7\u68C0\u67E5\u4EE3\u7801\u3002")}console.log("=".repeat(60)+"\n")}// ============================================================
435
+ */
436
+ function printValidationResult(result, showDetails = true) {
437
+ console.log('\n' + '='.repeat(60));
438
+ console.log('📊 埋点验证报告');
439
+ console.log('='.repeat(60));
440
+
441
+ // 统计信息
442
+ console.log(`\n📁 文件统计: 扫描 ${result.stats.scannedFiles}/${result.stats.totalFiles} 个文件`);
443
+ if (result.stats.engine || (result.stats.scanRoots && result.stats.scanRoots.length)) {
444
+ console.log(`🧭 引擎: ${result.stats.engine || 'default'} | 目录: ${(result.stats.scanRoots || []).join(', ') || '.'}`);
445
+ }
446
+ console.log(`🔗 集成类型: ${result.integrationType === 'sdk' ? 'SDK 集成' : result.integrationType === 'tracking' ? 'Tracking 直接集成' : '未检测到'}`);
447
+
448
+ // 方法检测结果
449
+ if (showDetails) {
450
+ console.log('\n📋 方法检测明细:');
451
+ console.log('-'.repeat(60));
452
+
453
+ const requiredMethods = result.methods.filter(m => m.required);
454
+ const optionalMethods = result.methods.filter(m => !m.required);
455
+
456
+ console.log('\n必须方法:');
457
+ for (const m of requiredMethods) {
458
+ const status = m.found ? '✅' : '❌';
459
+ console.log(` ${status} ${m.method.padEnd(35)} ${m.found ? '已调用' : '未调用'}`);
460
+ }
461
+
462
+ console.log('\n可选方法:');
463
+ for (const m of optionalMethods) {
464
+ const status = m.found ? '✅' : '⚠️ ';
465
+ console.log(` ${status} ${m.method.padEnd(35)} ${m.found ? '已调用' : '未调用'}`);
466
+ }
467
+ }
468
+
469
+ // 错误信息
470
+ if (result.errors.length > 0) {
471
+ console.log('\n❌ 错误:');
472
+ result.errors.forEach(err => console.log(` ${err}`));
473
+ }
474
+
475
+ // 警告信息
476
+ if (result.warnings.length > 0) {
477
+ console.log('\n⚠️ 警告:');
478
+ result.warnings.forEach(warn => console.log(` ${warn}`));
479
+ }
480
+
481
+ // 最终结果
482
+ console.log('\n' + '='.repeat(60));
483
+ if (result.success) {
484
+ console.log('✅ 验证通过!所有必须的埋点方法都已正确调用。');
485
+ } else {
486
+ console.log('❌ 验证失败!存在缺失的必须埋点方法,请检查代码。');
487
+ }
488
+ console.log('='.repeat(60) + '\n');
489
+ }
490
+
491
+ // ============================================================
108
492
  // 导出
109
493
  // ============================================================
110
- module.exports={// 核心验证函数
111
- validateTracking,printValidationResult,// 配置数据(供外部使用)
112
- TRACKING_METHODS,SDK_TO_TRACKING_MAP,// 工具函数(供外部使用)
113
- scanSourceFiles,detectDirectTrackingCall,detectSDKCall,hasTrackingImport,hasSDKImport};
494
+
495
+ module.exports = {
496
+ // 核心验证函数
497
+ validateTracking,
498
+ printValidationResult,
499
+
500
+ // 配置数据(供外部使用)
501
+ TRACKING_METHODS,
502
+ SDK_TO_TRACKING_MAP,
503
+ ENGINE_SCAN_ROOTS,
504
+ DEFAULT_EXCLUDE_DIRS,
505
+
506
+ // 工具函数(供外部使用)
507
+ detectProjectEngine,
508
+ resolveScanRoots,
509
+ scanSourceFiles,
510
+ detectDirectTrackingCall,
511
+ detectSDKCall,
512
+ hasTrackingImport,
513
+ hasSDKImport
514
+ };
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ const { describe, it, before, after } = 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 {
9
+ validateTracking,
10
+ detectProjectEngine,
11
+ resolveScanRoots,
12
+ hasSDKImport,
13
+ } = require('./tracking-validator.js');
14
+
15
+ const SDK_SOURCE = `
16
+ import { sdk } from '@playcraft/adsdk/cjs';
17
+ sdk.playcraftInit(() => {});
18
+ sdk.playcraftStart();
19
+ sdk.playcraftInteractive();
20
+ sdk.playcraftChallengeStart();
21
+ sdk.playcraftRecordSuccess();
22
+ sdk.playcraftRecordFailed();
23
+ sdk.playcraftInstall();
24
+ `;
25
+
26
+ describe('tracking validator engine scan roots', () => {
27
+ let root;
28
+
29
+ before(() => {
30
+ root = fs.mkdtempSync(path.join(os.tmpdir(), 'tracking-scan-'));
31
+ fs.mkdirSync(path.join(root, 'assets/scripts/game'), { recursive: true });
32
+ fs.mkdirSync(path.join(root, 'assets/scenes'), { recursive: true });
33
+ fs.mkdirSync(path.join(root, 'library/import'), { recursive: true });
34
+ fs.mkdirSync(path.join(root, 'src'), { recursive: true });
35
+ fs.writeFileSync(path.join(root, 'builds.config.js'), "module.exports = { engine: 'cocos' };\n");
36
+ fs.writeFileSync(path.join(root, 'assets/scripts/game/GameRoot.ts'), SDK_SOURCE);
37
+ fs.writeFileSync(
38
+ path.join(root, 'library/import/noise.js'),
39
+ "import { tracking } from '@playcraft/ads-tracking';\ntracking.onPlaycraftTrackingInit();\n",
40
+ );
41
+ fs.writeFileSync(path.join(root, 'src/index.ts'), 'export const unused = 1;\n');
42
+ });
43
+
44
+ after(() => {
45
+ fs.rmSync(root, { recursive: true, force: true });
46
+ });
47
+
48
+ it('treats @playcraft/adsdk/cjs as an SDK import', () => {
49
+ assert.equal(hasSDKImport("import * as adsdk from '@playcraft/adsdk/cjs';"), true);
50
+ assert.equal(hasSDKImport("import { sdk } from '@playcraft/adsdk';"), true);
51
+ });
52
+
53
+ it('detects cocos from builds.config.js and scans assets/scripts only', () => {
54
+ assert.equal(detectProjectEngine(root), 'cocos');
55
+ const roots = resolveScanRoots(root, 'cocos').map((dir) => path.relative(root, dir));
56
+ assert.deepEqual(roots, ['assets/scripts']);
57
+ });
58
+
59
+ it('passes required tracking checks for a cocos SDK wrapper project', () => {
60
+ const result = validateTracking(root, { engine: 'cocos' });
61
+ assert.equal(result.stats.engine, 'cocos');
62
+ assert.deepEqual(result.stats.scanRoots, ['assets/scripts']);
63
+ assert.equal(result.integrationType, 'sdk');
64
+ assert.equal(result.success, true, result.errors.join('\n'));
65
+ assert.equal(result.stats.hasSDKImport, true);
66
+ });
67
+
68
+ it('does not let library ads-tracking imports flip the integration type', () => {
69
+ const result = validateTracking(root, { engine: 'cocos' });
70
+ assert.equal(result.stats.hasTrackingImport, false);
71
+ assert.ok(result.stats.scannedFiles < 10);
72
+ });
73
+
74
+ it('scans src for phaser', () => {
75
+ const phaserRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'tracking-phaser-'));
76
+ fs.mkdirSync(path.join(phaserRoot, 'src'), { recursive: true });
77
+ fs.mkdirSync(path.join(phaserRoot, 'library'), { recursive: true });
78
+ fs.writeFileSync(path.join(phaserRoot, 'src/main.ts'), SDK_SOURCE.replace('/cjs', ''));
79
+ fs.writeFileSync(
80
+ path.join(phaserRoot, 'library/noise.js'),
81
+ "import { tracking } from '@playcraft/ads-tracking';\n",
82
+ );
83
+ try {
84
+ const result = validateTracking(phaserRoot, { engine: 'phaser' });
85
+ assert.deepEqual(result.stats.scanRoots, ['src']);
86
+ assert.equal(result.integrationType, 'sdk');
87
+ assert.equal(result.success, true, result.errors.join('\n'));
88
+ } finally {
89
+ fs.rmSync(phaserRoot, { recursive: true, force: true });
90
+ }
91
+ });
92
+ });
package/core/vite.dev.js CHANGED
@@ -1 +1 @@
1
- "use strict";module.exports=require("./engines/web/vite.dev.js");
1
+ module.exports = require('./engines/web/vite.dev.js');
@@ -1 +1 @@
1
- "use strict";module.exports=require("./engines/web/webpack.build.js");
1
+ module.exports = require('./engines/web/webpack.build.js');
@@ -1 +1 @@
1
- "use strict";module.exports=require("./engines/web/webpack.common.js");
1
+ module.exports = require('./engines/web/webpack.common.js');
@@ -1 +1 @@
1
- "use strict";module.exports=require("./engines/web/webpack.dev.js");
1
+ module.exports = require('./engines/web/webpack.dev.js');