@playcraft/devkit 1.0.14 → 1.0.15-beta.1

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.
@@ -1,4 +1,4 @@
1
- "use strict";process.env.BABEL_ENV="production";process.env.NODE_ENV="production";const fs=require("fs");const path=require("path");const{runBuild}=require("../../core/webpack.build.js");const{runThemeValidation,restoreSchema}=require("../../core/utils/validateThemeData");/**
1
+ "use strict";process.env.BABEL_ENV="production";process.env.NODE_ENV="production";const fs=require("fs");const path=require("path");const{runBuild}=require("../../core/webpack.build.js");const{runThemeValidation,restoreSchema}=require("../../core/utils/validateThemeData");const{runPreBuildChecks}=require("../../core/validators/pre-build-checker.js");/**
2
2
  * 从主题入口文件中检测当前主题名称。
3
3
  * 支持以下模式:
4
4
  * export { default } from './themeName';
@@ -7,7 +7,15 @@
7
7
  const buildsConfigPath=path.join(projectRoot,"builds.config.js");let themeConfig={sourceDir:"src/theme",entryFile:"src/theme/index.ts"};if(fs.existsSync(buildsConfigPath)){try{delete require.cache[buildsConfigPath];const config=require(buildsConfigPath);if(config.themes){themeConfig={...themeConfig,...config.themes}}}catch(e){// 忽略错误
8
8
  }}const entryPath=path.join(projectRoot,themeConfig.entryFile);if(!fs.existsSync(entryPath)){return{themeName:null,themeConfig}}const content=fs.readFileSync(entryPath,"utf8");// 匹配: export { default } from './themeName' 或 import ... from './themeName'
9
9
  const match=content.match(/from\s+['"]\.\/([^'"]+)['"]/);if(match){return{themeName:match[1],themeConfig}}return{themeName:null,themeConfig}}// --- 主流程 ---
10
- const projectRoot=process.cwd();const{themeName,themeConfig}=detectCurrentTheme(projectRoot);let schemaPathToRestore=null;// In batch mode (PLAYABLE_BATCH_MODE=1), the parent process has already
10
+ const projectRoot=process.cwd();const{themeName,themeConfig}=detectCurrentTheme(projectRoot);let schemaPathToRestore=null;// ============================================================
11
+ // 📌 编译前检查(埋点验证等)
12
+ // ============================================================
13
+ // 支持通过环境变量控制是否跳过检查
14
+ // - SKIP_PRE_BUILD_CHECKS=1: 强制跳过检查
15
+ // - PLAYABLE_BATCH_MODE=1: 批量构建模式,子进程自动跳过检查(主进程已执行)
16
+ const isBatchMode=process.env.PLAYABLE_BATCH_MODE==="1";const shouldSkipChecks=process.env.SKIP_PRE_BUILD_CHECKS==="1"||isBatchMode;async function runPreChecks(){if(shouldSkipChecks){console.log("\n\u26A0\uFE0F \u8DF3\u8FC7\u7F16\u8BD1\u524D\u68C0\u67E5\uFF08SKIP_PRE_BUILD_CHECKS=1\uFF09\n");return}const checkResult=await runPreBuildChecks(projectRoot,{verbose:false,showDetails:true,failFast:false});if(!checkResult.success){console.error("\n\u274C \u7F16\u8BD1\u524D\u68C0\u67E5\u5931\u8D25\uFF0C\u7EC8\u6B62\u6784\u5EFA\u3002");console.error("\u63D0\u793A\uFF1A\u5982\u9700\u8DF3\u8FC7\u68C0\u67E5\uFF0C\u53EF\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF: SKIP_PRE_BUILD_CHECKS=1\n");process.exit(1)}}// 执行编译前检查(异步)
17
+ runPreChecks().then(()=>{// 检查通过后继续原有流程
18
+ continueWithBuild()}).catch(err=>{console.error("\n\u274C \u7F16\u8BD1\u524D\u68C0\u67E5\u51FA\u9519:",err);process.exit(1)});function continueWithBuild(){// In batch mode (PLAYABLE_BATCH_MODE=1), the parent process has already
11
19
  // executed runThemeValidation and overwritten theme.schema.json5 with merged data.
12
20
  // Running it again in child processes would cause a race condition:
13
21
  // 1. Child reads the already-overwritten schema (pure data, no "properties"/"default")
@@ -15,4 +23,4 @@ const projectRoot=process.cwd();const{themeName,themeConfig}=detectCurrentTheme(
15
23
  // 3. Child overwrites schema with {} → file becomes empty object
16
24
  const isBatchMode=process.env.PLAYABLE_BATCH_MODE==="1";if(themeName&&!isBatchMode){const buildLog=(msg,level)=>{const colors={info:"\x1B[36m",success:"\x1B[32m",error:"\x1B[31m"};const reset="\x1B[0m";const color=colors[level]||"";console.log(`${color}[build] ${msg}${reset}`)};const{valid,schemaPath}=runThemeValidation(themeName,projectRoot,themeConfig,{log:buildLog});schemaPathToRestore=schemaPath;if(!valid){// 校验失败时恢复 schema(备份可能已创建)
17
25
  if(schemaPathToRestore)restoreSchema(schemaPathToRestore);process.exit(1)}}// 执行构建,无论成功或失败都恢复 schema
18
- runBuild().then(()=>{if(schemaPathToRestore)restoreSchema(schemaPathToRestore)}).catch(err=>{if(schemaPathToRestore)restoreSchema(schemaPathToRestore);console.error(err);process.exit(1)});
26
+ runBuild().then(()=>{if(schemaPathToRestore)restoreSchema(schemaPathToRestore)}).catch(err=>{if(schemaPathToRestore)restoreSchema(schemaPathToRestore);console.error(err);process.exit(1)})}
@@ -7,7 +7,7 @@
7
7
  * 2. 每个构建任务使用独立的临时目录(dist-temp-{taskId})
8
8
  * 3. 构建完成后自动移动到目标目录并清理临时目录
9
9
  * 4. 支持灵活的配置系统
10
- */const{execSync,exec}=require("child_process");const fs=require("fs");const path=require("path");const os=require("os");const{promisify}=require("util");const{fetchTemporaryCredentials,createCOSClient,uploadDirectoryToCOS,uploadFileToCOS,purgeCDNCache}=require("./cos-uploader");const{runThemeValidation,restoreSchema}=require("./utils/validateThemeData");const execAsync=promisify(exec);/**
10
+ */const{execSync,exec}=require("child_process");const fs=require("fs");const path=require("path");const os=require("os");const{promisify}=require("util");const{fetchTemporaryCredentials,createCOSClient,uploadDirectoryToCOS,uploadFileToCOS,purgeCDNCache}=require("./cos-uploader");const{runThemeValidation,restoreSchema}=require("./utils/validateThemeData");const{runPreBuildChecks}=require("./validators/pre-build-checker.js");const execAsync=promisify(exec);/**
11
11
  * 更新 preview.json 文件
12
12
  */async function updatePreviewJson(cosConfig,cosPrefix,currentTheme,previewLinks,credentials=null){const previewJsonKey=cosPrefix?`${cosPrefix}/preview.json`:"preview.json";// 如果没有传入凭证,尝试获取一次
13
13
  if(!credentials){try{credentials=await fetchTemporaryCredentials()}catch(error){log(`获取临时凭证失败,跳过 preview.json 更新: ${error.message}`,"warn");return}}// 读取现有的 preview.json 文件
@@ -135,7 +135,10 @@ if(buildJson.filename){log(" \u26A0\uFE0F build.json.filename \u5C06\u88AB\u5F
135
135
  */function mergeConfig(defaultConfig,userConfig){const result={...defaultConfig};for(const key in userConfig){if(typeof userConfig[key]==="object"&&!Array.isArray(userConfig[key])){result[key]={...defaultConfig[key],...userConfig[key]}}else{result[key]=userConfig[key]}}return result}/**
136
136
  * 打印配置摘要
137
137
  */function printSummary(config,themes,channels){var _os$cpus$;const totalBuilds=themes.length*channels.length;const cpuCount=os.cpus().length;const cpuModel=((_os$cpus$=os.cpus()[0])===null||_os$cpus$===void 0?void 0:_os$cpus$.model)||"Unknown";log("========================================");log("\u5F00\u59CB\u6279\u91CF\u6784\u5EFA\u4EFB\u52A1");log(`项目: ${config.title}`);log(`主题数: ${themes.length}`);log(`渠道数: ${channels.length}`);log(`总构建数: ${totalBuilds}`);log(`CPU: ${cpuCount} 核心 (${cpuModel.substring(0,40)}${cpuModel.length>40?"...":""})`);log(`并发数: ${config.build.parallel} (自动优化)`);log("========================================\n");if(themes.length<=10){log(`主题列表: ${themes.join(", ")}`,"debug")}log(`渠道列表: ${channels.join(", ")}`,"debug");log("")}// ==================== 主函数 ====================
138
- async function buildBatch(){const projectRoot=process.cwd();const startTime=Date.now();try{// 加载配置(包含远程配置合并)
138
+ async function buildBatch(){const projectRoot=process.cwd();const startTime=Date.now();try{// ============================================================
139
+ // 📌 编译前检查(埋点验证等)- 仅在主进程执行一次
140
+ // ============================================================
141
+ const shouldSkipChecks=process.env.SKIP_PRE_BUILD_CHECKS==="1";if(!shouldSkipChecks){log("\u6267\u884C\u7F16\u8BD1\u524D\u68C0\u67E5\uFF08\u57CB\u70B9\u9A8C\u8BC1\u7B49\uFF09...","info");const checkResult=await runPreBuildChecks(projectRoot,{verbose:false,showDetails:true,failFast:false});if(!checkResult.success){log("\u7F16\u8BD1\u524D\u68C0\u67E5\u5931\u8D25\uFF0C\u7EC8\u6B62\u6279\u91CF\u6784\u5EFA\u3002","error");log("\u63D0\u793A\uFF1A\u5982\u9700\u8DF3\u8FC7\u68C0\u67E5\uFF0C\u53EF\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF: SKIP_PRE_BUILD_CHECKS=1","warn");process.exit(1)}log("\u2713 \u7F16\u8BD1\u524D\u68C0\u67E5\u901A\u8FC7","success")}else{log("\u8DF3\u8FC7\u7F16\u8BD1\u524D\u68C0\u67E5\uFF08SKIP_PRE_BUILD_CHECKS=1\uFF09","warn")}// 加载配置(包含远程配置合并)
139
142
  const config=await loadConfig(projectRoot);// 解析命令行参数
140
143
  const args=parseArgs();// 应用命令行参数
141
144
  if(args.parallel!==null){config.build.parallel=args.parallel}// 是否启用渠道折叠为 zip 模式
package/core/index.js CHANGED
@@ -1 +1,4 @@
1
- "use strict";const{allowedExtensions,webpackCommonConfig}=require("./webpack.common.js");const{parseArgvOptions}=require("./utils/parseArgvOptions.js");const{mergeOptions}=require("./utils/mergeOptions.js");const{options}=require("./options.js");const{DAPIInjectorPlugin}=require("./plugins/DAPIInjectorPlugin.js");const{DebuggerInjectionPlugin}=require("./plugins/DebuggerInjectionPlugin.js");const{ExitAPIInjectorPlugin}=require("./plugins/ExitAPIInjectorPlugin.js");const{runBuild,makeWebpackBuildConfig}=require("./webpack.build.js");const{runDev,makeWebpackDevConfig}=require("./webpack.dev.js");const{runViteDev,makeViteDevConfig}=require("./vite.dev.js");exports.mergeOptions=mergeOptions;exports.parseArgvOptions=parseArgvOptions;exports.options=options;exports.allowedExtensions=allowedExtensions;exports.webpackCommonConfig=webpackCommonConfig;exports.makeWebpackDevConfig=makeWebpackDevConfig;exports.makeWebpackBuildConfig=makeWebpackBuildConfig;exports.runDev=runDev;exports.runBuild=runBuild;exports.DAPIInjectorPlugin=DAPIInjectorPlugin;exports.ExitAPIInjectorPlugin=ExitAPIInjectorPlugin;exports.DebuggerInjectionPlugin=DebuggerInjectionPlugin;exports.makeViteDevConfig=makeViteDevConfig;exports.runViteDev=runViteDev;
1
+ "use strict";const{allowedExtensions,webpackCommonConfig}=require("./webpack.common.js");const{parseArgvOptions}=require("./utils/parseArgvOptions.js");const{mergeOptions}=require("./utils/mergeOptions.js");const{options}=require("./options.js");const{DAPIInjectorPlugin}=require("./plugins/DAPIInjectorPlugin.js");const{DebuggerInjectionPlugin}=require("./plugins/DebuggerInjectionPlugin.js");const{ExitAPIInjectorPlugin}=require("./plugins/ExitAPIInjectorPlugin.js");const{runBuild,makeWebpackBuildConfig}=require("./webpack.build.js");const{runDev,makeWebpackDevConfig}=require("./webpack.dev.js");const{runViteDev,makeViteDevConfig}=require("./vite.dev.js");// 验证器
2
+ const{runPreBuildChecks,runTrackingCheck,registerChecker,toggleChecker,listCheckers}=require("./validators/pre-build-checker.js");const{validateTracking,printValidationResult,TRACKING_METHODS,SDK_TO_TRACKING_MAP}=require("./validators/tracking-validator.js");exports.mergeOptions=mergeOptions;exports.parseArgvOptions=parseArgvOptions;exports.options=options;exports.allowedExtensions=allowedExtensions;exports.webpackCommonConfig=webpackCommonConfig;exports.makeWebpackDevConfig=makeWebpackDevConfig;exports.makeWebpackBuildConfig=makeWebpackBuildConfig;exports.runDev=runDev;exports.runBuild=runBuild;exports.DAPIInjectorPlugin=DAPIInjectorPlugin;exports.ExitAPIInjectorPlugin=ExitAPIInjectorPlugin;exports.DebuggerInjectionPlugin=DebuggerInjectionPlugin;exports.makeViteDevConfig=makeViteDevConfig;exports.runViteDev=runViteDev;// 编译前检查器导出
3
+ exports.runPreBuildChecks=runPreBuildChecks;exports.runTrackingCheck=runTrackingCheck;exports.registerChecker=registerChecker;exports.toggleChecker=toggleChecker;exports.listCheckers=listCheckers;// 埋点验证器导出
4
+ exports.validateTracking=validateTracking;exports.printValidationResult=printValidationResult;exports.TRACKING_METHODS=TRACKING_METHODS;exports.SDK_TO_TRACKING_MAP=SDK_TO_TRACKING_MAP;
@@ -0,0 +1,242 @@
1
+ // ============================================================
2
+ // 埋点验证器类型定义
3
+ // ============================================================
4
+
5
+ /**
6
+ * 埋点方法配置
7
+ */
8
+ export interface TrackingMethod {
9
+ /** 方法名 */
10
+ name: string;
11
+ /** 是否必须调用 */
12
+ required: boolean;
13
+ /** 方法描述 */
14
+ description: string;
15
+ /** 对应的渠道埋点 */
16
+ channels: string[];
17
+ }
18
+
19
+ /**
20
+ * 验证结果
21
+ */
22
+ export interface ValidationResult {
23
+ /** 验证是否通过 */
24
+ success: boolean;
25
+ /** 集成类型 */
26
+ integrationType: 'sdk' | 'tracking' | 'none';
27
+ /** 方法检测结果 */
28
+ methods: Array<{
29
+ method: string;
30
+ found: boolean;
31
+ required: boolean;
32
+ description: string;
33
+ }>;
34
+ /** 错误信息列表 */
35
+ errors: string[];
36
+ /** 警告信息列表 */
37
+ warnings: string[];
38
+ /** 统计信息 */
39
+ stats: {
40
+ totalFiles: number;
41
+ scannedFiles: number;
42
+ hasTrackingImport: boolean;
43
+ hasSDKImport: boolean;
44
+ };
45
+ }
46
+
47
+ /**
48
+ * 验证选项
49
+ */
50
+ export interface ValidationOptions {
51
+ /** 文件扩展名列表 */
52
+ extensions?: string[];
53
+ /** 排除的目录列表 */
54
+ excludeDirs?: string[];
55
+ /** 是否输出详细信息 */
56
+ verbose?: boolean;
57
+ }
58
+
59
+ /**
60
+ * 验证项目中的埋点调用
61
+ * @param projectRoot - 项目根目录
62
+ * @param options - 验证选项
63
+ */
64
+ export declare function validateTracking(
65
+ projectRoot: string,
66
+ options?: ValidationOptions
67
+ ): ValidationResult;
68
+
69
+ /**
70
+ * 格式化验证结果并输出到控制台
71
+ * @param result - 验证结果
72
+ * @param showDetails - 是否显示详细信息
73
+ */
74
+ export declare function printValidationResult(
75
+ result: ValidationResult,
76
+ showDetails?: boolean
77
+ ): void;
78
+
79
+ /**
80
+ * 扫描目录获取所有源码文件
81
+ * @param dir - 目录路径
82
+ * @param extensions - 文件扩展名列表
83
+ * @param excludeDirs - 排除的目录名列表
84
+ */
85
+ export declare function scanSourceFiles(
86
+ dir: string,
87
+ extensions?: string[],
88
+ excludeDirs?: string[]
89
+ ): string[];
90
+
91
+ /**
92
+ * 检测代码中是否包含 tracking 方法的直接调用
93
+ * @param content - 文件内容
94
+ * @param methodName - 方法名
95
+ */
96
+ export declare function detectDirectTrackingCall(
97
+ content: string,
98
+ methodName: string
99
+ ): boolean;
100
+
101
+ /**
102
+ * 检测代码中是否包含 SDK 方法调用
103
+ * @param content - 文件内容
104
+ * @param sdkMethod - SDK 方法名
105
+ */
106
+ export declare function detectSDKCall(
107
+ content: string,
108
+ sdkMethod: string
109
+ ): boolean;
110
+
111
+ /**
112
+ * 检测是否导入了 @playcraft/ads-tracking
113
+ * @param content - 文件内容
114
+ */
115
+ export declare function hasTrackingImport(content: string): boolean;
116
+
117
+ /**
118
+ * 检测是否导入了 @playcraft/adsdk
119
+ * @param content - 文件内容
120
+ */
121
+ export declare function hasSDKImport(content: string): boolean;
122
+
123
+ /**
124
+ * 所有埋点方法配置
125
+ */
126
+ export declare const TRACKING_METHODS: TrackingMethod[];
127
+
128
+ /**
129
+ * SDK 静态方法映射到 tracking 方法
130
+ */
131
+ export declare const SDK_TO_TRACKING_MAP: Record<string, string>;
132
+
133
+ // ============================================================
134
+ // 编译前检查器类型定义
135
+ // ============================================================
136
+
137
+ /**
138
+ * 检查结果
139
+ */
140
+ export interface CheckResult {
141
+ /** 是否通过 */
142
+ success: boolean;
143
+ /** 结果消息 */
144
+ message: string;
145
+ /** 额外数据 */
146
+ data?: any;
147
+ /** 错误对象(如果检查出错) */
148
+ error?: Error;
149
+ }
150
+
151
+ /**
152
+ * 检查器定义
153
+ */
154
+ export interface Checker {
155
+ /** 检查器名称 */
156
+ name: string;
157
+ /** 检查器描述 */
158
+ description: string;
159
+ /** 是否启用 */
160
+ enabled: boolean;
161
+ /** 验证函数 */
162
+ validator: (projectRoot: string, options: any) => Promise<CheckResult> | CheckResult;
163
+ }
164
+
165
+ /**
166
+ * 编译前检查选项
167
+ */
168
+ export interface PreBuildCheckOptions {
169
+ /** 只执行指定的检查器 ID */
170
+ only?: string[];
171
+ /** 跳过指定的检查器 ID */
172
+ skip?: string[];
173
+ /** 详细输出 */
174
+ verbose?: boolean;
175
+ /** 显示详细报告 */
176
+ showDetails?: boolean;
177
+ /** 遇到错误立即停止 */
178
+ failFast?: boolean;
179
+ /** 文件扩展名(传递给检查器) */
180
+ extensions?: string[];
181
+ /** 排除的目录(传递给检查器) */
182
+ excludeDirs?: string[];
183
+ }
184
+
185
+ /**
186
+ * 编译前检查结果
187
+ */
188
+ export interface PreBuildCheckResult {
189
+ /** 是否全部通过 */
190
+ success: boolean;
191
+ /** 各检查器的结果 */
192
+ results: Map<string, CheckResult>;
193
+ }
194
+
195
+ /**
196
+ * 执行所有已启用的编译前检查器
197
+ * @param projectRoot - 项目根目录
198
+ * @param options - 选项
199
+ */
200
+ export declare function runPreBuildChecks(
201
+ projectRoot: string,
202
+ options?: PreBuildCheckOptions
203
+ ): Promise<PreBuildCheckResult>;
204
+
205
+ /**
206
+ * 单独运行埋点验证(快捷方法)
207
+ * @param projectRoot - 项目根目录
208
+ * @param options - 选项
209
+ */
210
+ export declare function runTrackingCheck(
211
+ projectRoot: string,
212
+ options?: ValidationOptions
213
+ ): Promise<CheckResult>;
214
+
215
+ /**
216
+ * 注册自定义检查器
217
+ * @param id - 检查器 ID
218
+ * @param checker - 检查器配置
219
+ */
220
+ export declare function registerChecker(id: string, checker: Checker): void;
221
+
222
+ /**
223
+ * 启用/禁用检查器
224
+ * @param id - 检查器 ID
225
+ * @param enabled - 是否启用
226
+ */
227
+ export declare function toggleChecker(id: string, enabled: boolean): void;
228
+
229
+ /**
230
+ * 获取所有检查器列表
231
+ */
232
+ export declare function listCheckers(): Array<{
233
+ id: string;
234
+ name: string;
235
+ description: string;
236
+ enabled: boolean;
237
+ }>;
238
+
239
+ /**
240
+ * 内部检查器注册表(高级用户使用)
241
+ */
242
+ export declare const checkers: Map<string, Checker>;
@@ -0,0 +1,89 @@
1
+ "use strict";const{validateTracking,printValidationResult}=require("./tracking-validator.js");// ============================================================
2
+ // 检查器注册表
3
+ // ============================================================
4
+ /**
5
+ * 检查器定义
6
+ * @typedef {Object} Checker
7
+ * @property {string} name - 检查器名称
8
+ * @property {string} description - 检查器描述
9
+ * @property {boolean} enabled - 是否启用
10
+ * @property {(projectRoot: string, options: any) => Promise<CheckResult>|CheckResult} validator - 验证函数
11
+ *//**
12
+ * 检查结果
13
+ * @typedef {Object} CheckResult
14
+ * @property {boolean} success - 是否通过
15
+ * @property {string} message - 结果消息
16
+ * @property {any} [data] - 额外数据
17
+ *//**
18
+ * 所有注册的检查器
19
+ * @type {Map<string, Checker>}
20
+ */const checkers=new Map;// ============================================================
21
+ // 内置检查器
22
+ // ============================================================
23
+ /**
24
+ * 埋点验证检查器
25
+ */checkers.set("tracking",{name:"\u57CB\u70B9\u9A8C\u8BC1",description:"\u9A8C\u8BC1\u9879\u76EE\u4E2D\u662F\u5426\u6B63\u786E\u8C03\u7528\u4E86\u6240\u6709\u5FC5\u987B\u7684\u57CB\u70B9\u65B9\u6CD5",enabled:true,validator:(projectRoot,options={})=>{const result=validateTracking(projectRoot,{verbose:options.verbose||false,extensions:options.extensions,excludeDirs:options.excludeDirs});return{success:result.success,message:result.success?"\u2705 \u57CB\u70B9\u9A8C\u8BC1\u901A\u8FC7":`❌ 埋点验证失败:缺少 ${result.errors.length} 个必须的埋点方法`,data:result}}});// 预留:未来可添加更多检查器
26
+ // checkers.set('assets', {
27
+ // name: '资源检查',
28
+ // description: '验证项目资源文件的完整性和规范性',
29
+ // enabled: false,
30
+ // validator: (projectRoot, options) => {
31
+ // // TODO: 实现资源检查逻辑
32
+ // return { success: true, message: '资源检查通过' };
33
+ // }
34
+ // });
35
+ // checkers.set('config', {
36
+ // name: '配置检查',
37
+ // description: '验证项目配置文件的正确性',
38
+ // enabled: false,
39
+ // validator: (projectRoot, options) => {
40
+ // // TODO: 实现配置检查逻辑
41
+ // return { success: true, message: '配置检查通过' };
42
+ // }
43
+ // });
44
+ // ============================================================
45
+ // 检查器管理
46
+ // ============================================================
47
+ /**
48
+ * 注册自定义检查器
49
+ * @param {string} id - 检查器 ID
50
+ * @param {Checker} checker - 检查器配置
51
+ */function registerChecker(id,checker){if(checkers.has(id)){console.warn(`[警告] 检查器 "${id}" 已存在,将被覆盖`)}checkers.set(id,checker)}/**
52
+ * 启用/禁用检查器
53
+ * @param {string} id - 检查器 ID
54
+ * @param {boolean} enabled - 是否启用
55
+ */function toggleChecker(id,enabled){const checker=checkers.get(id);if(checker){checker.enabled=enabled}else{console.warn(`[警告] 未找到检查器: ${id}`)}}/**
56
+ * 获取所有检查器列表
57
+ * @returns {Array<{id: string, name: string, description: string, enabled: boolean}>}
58
+ */function listCheckers(){return Array.from(checkers.entries()).map(([id,checker])=>({id,name:checker.name,description:checker.description,enabled:checker.enabled}))}// ============================================================
59
+ // 执行检查
60
+ // ============================================================
61
+ /**
62
+ * 执行所有已启用的检查器
63
+ * @param {string} projectRoot - 项目根目录
64
+ * @param {Object} [options] - 选项
65
+ * @param {string[]} [options.only] - 只执行指定的检查器 ID
66
+ * @param {string[]} [options.skip] - 跳过指定的检查器 ID
67
+ * @param {boolean} [options.verbose] - 详细输出
68
+ * @param {boolean} [options.showDetails] - 显示详细报告
69
+ * @param {boolean} [options.failFast] - 遇到错误立即停止
70
+ * @returns {Promise<{success: boolean, results: Map<string, CheckResult>}>}
71
+ */async function runPreBuildChecks(projectRoot,options={}){const{only=[],skip=[],verbose=false,showDetails=true,failFast=false}=options;const results=new Map;let allSuccess=true;console.log("\n"+"\u2550".repeat(70));console.log("\uD83D\uDD0D \u7F16\u8BD1\u524D\u68C0\u67E5");console.log("\u2550".repeat(70));// 确定要执行的检查器
72
+ let checkersToRun=Array.from(checkers.entries()).filter(([id,checker])=>{// 如果指定了 only,只运行指定的
73
+ if(only.length>0&&!only.includes(id))return false;// 跳过 skip 列表中的
74
+ if(skip.includes(id))return false;// 只运行启用的
75
+ return checker.enabled});if(checkersToRun.length===0){console.log("\n\u26A0\uFE0F \u6CA1\u6709\u542F\u7528\u7684\u68C0\u67E5\u5668");console.log("\u2550".repeat(70)+"\n");return{success:true,results}}if(verbose){console.log(`\n📋 将执行 ${checkersToRun.length} 个检查器:`);checkersToRun.forEach(([id,checker])=>{console.log(` - ${checker.name} (${id})`)});console.log("")}// 执行检查器
76
+ for(const[id,checker]of checkersToRun){try{console.log(`\n▶️ 执行检查: ${checker.name}`);console.log("-".repeat(70));const result=await Promise.resolve(checker.validator(projectRoot,options));results.set(id,result);console.log(result.message);// 详细输出(针对埋点验证)
77
+ if(showDetails&&id==="tracking"&&result.data){printValidationResult(result.data,showDetails)}if(!result.success){allSuccess=false;if(failFast){console.log("\n\u26A0\uFE0F failFast \u6A21\u5F0F\uFF1A\u68C0\u6D4B\u5230\u5931\u8D25\uFF0C\u7EC8\u6B62\u540E\u7EED\u68C0\u67E5");break}}}catch(error){console.error(`\n❌ 检查器 "${checker.name}" 执行失败:`,error.message);results.set(id,{success:false,message:`执行出错: ${error.message}`,error});allSuccess=false;if(failFast)break}}// 总结
78
+ console.log("\n"+"\u2550".repeat(70));console.log("\uD83D\uDCCA \u68C0\u67E5\u603B\u7ED3");console.log("\u2550".repeat(70));const successCount=Array.from(results.values()).filter(r=>r.success).length;const totalCount=results.size;console.log(`\n通过: ${successCount}/${totalCount}`);if(allSuccess){console.log("\n\u2705 \u6240\u6709\u68C0\u67E5\u901A\u8FC7\uFF0C\u53EF\u4EE5\u7EE7\u7EED\u7F16\u8BD1\uFF01")}else{console.log("\n\u274C \u90E8\u5206\u68C0\u67E5\u5931\u8D25\uFF0C\u8BF7\u4FEE\u590D\u540E\u518D\u7F16\u8BD1\u3002")}console.log("\u2550".repeat(70)+"\n");return{success:allSuccess,results}}/**
79
+ * 单独运行埋点验证(快捷方法)
80
+ * @param {string} projectRoot - 项目根目录
81
+ * @param {Object} [options] - 选项
82
+ * @returns {Promise<CheckResult>}
83
+ */async function runTrackingCheck(projectRoot,options={}){const checker=checkers.get("tracking");if(!checker){throw new Error("\u57CB\u70B9\u68C0\u67E5\u5668\u672A\u6CE8\u518C")}return await Promise.resolve(checker.validator(projectRoot,options))}// ============================================================
84
+ // 导出
85
+ // ============================================================
86
+ module.exports={// 主要函数
87
+ runPreBuildChecks,runTrackingCheck,// 检查器管理
88
+ registerChecker,toggleChecker,listCheckers,// 内部访问(供高级用户)
89
+ checkers};
@@ -0,0 +1,113 @@
1
+ "use strict";const fs=require("fs");const path=require("path");// ============================================================
2
+ // 埋点方法配置
3
+ // ============================================================
4
+ /**
5
+ * 埋点方法配置
6
+ * @typedef {Object} TrackingMethod
7
+ * @property {string} name - 方法名
8
+ * @property {boolean} required - 是否必须调用
9
+ * @property {string} description - 方法描述
10
+ * @property {string[]} channels - 对应的渠道埋点
11
+ *//**
12
+ * 所有埋点方法配置
13
+ * @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"]}];/**
15
+ * SDK 静态方法映射到 tracking 方法
16
+ * 这些 SDK 方法内部会调用对应的 tracking 方法
17
+ */const SDK_TO_TRACKING_MAP={"playcraftInit":"onPlaycraftTrackingInit","playcraftStart":"onPlaycraftTrackingLoading","playcraftInteractive":"onPlaycraftTrackingLoaded","playcraftChallengeStart":"onPlaycraftChallengeStart","recordPlaycraftChallengeProgress":"onPlaycraftChallengeProgress","playcraftRecordSuccess":"onPlaycraftChallengeSuccess","playcraftFinish":"onPlaycraftChallengeFinish","playcraftInstall":"onPlaycraftInstall","playcraftRetry":"onPlaycraftRetry","playcraftRecordFailed":"onPlaycraftChallengeFailed"};// ============================================================
18
+ // 文件扫描工具
19
+ // ============================================================
20
+ /**
21
+ * 递归扫描目录获取所有源码文件
22
+ * @param {string} dir - 目录路径
23
+ * @param {string[]} extensions - 文件扩展名列表
24
+ * @param {string[]} excludeDirs - 排除的目录名列表
25
+ * @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}/**
29
+ * 读取文件内容(带错误处理)
30
+ * @param {string} filePath - 文件路径
31
+ * @returns {string|null} 文件内容或 null
32
+ */function readFileContent(filePath){try{return fs.readFileSync(filePath,"utf-8")}catch(err){console.warn(`[警告] 无法读取文件: ${filePath}`,err.message);return null}}// ============================================================
33
+ // 方法调用检测
34
+ // ============================================================
35
+ /**
36
+ * 检测代码中是否包含 tracking 方法的直接调用
37
+ * @param {string} content - 文件内容
38
+ * @param {string} methodName - 方法名
39
+ * @returns {boolean} 是否找到调用
40
+ *
41
+ * 注意:不依赖对象名称,只检测方法调用模式
42
+ * 匹配: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))}/**
47
+ * 检测代码中是否包含 SDK 方法调用(会间接触发 tracking)
48
+ * @param {string} content - 文件内容
49
+ * @param {string} sdkMethod - SDK 方法名
50
+ * @returns {boolean} 是否找到调用
51
+ *
52
+ * 注意:不依赖对象名称,只检测方法调用模式
53
+ * 匹配: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))}/**
58
+ * 检测是否导入了 @playcraft/ads-tracking
59
+ * @param {string} content - 文件内容
60
+ * @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)}// ============================================================
66
+ // 验证核心逻辑
67
+ // ============================================================
68
+ /**
69
+ * 验证结果
70
+ * @typedef {Object} ValidationResult
71
+ * @property {boolean} success - 验证是否通过
72
+ * @property {'sdk'|'tracking'|'none'} integrationType - 集成类型
73
+ * @property {Array<{method: string, found: boolean, required: boolean, description: string}>} methods - 方法检测结果
74
+ * @property {string[]} errors - 错误信息列表
75
+ * @property {string[]} warnings - 警告信息列表
76
+ * @property {Object} stats - 统计信息
77
+ *//**
78
+ * 验证项目中的埋点调用
79
+ * @param {string} projectRoot - 项目根目录
80
+ * @param {Object} [options] - 验证选项
81
+ * @param {string[]} [options.extensions] - 文件扩展名
82
+ * @param {string[]} [options.excludeDirs] - 排除的目录
83
+ * @param {boolean} [options.verbose] - 是否输出详细信息
84
+ * @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}// ============================================================
96
+ // 格式化输出
97
+ // ============================================================
98
+ /**
99
+ * 格式化验证结果并输出到控制台
100
+ * @param {ValidationResult} result - 验证结果
101
+ * @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")}// ============================================================
108
+ // 导出
109
+ // ============================================================
110
+ module.exports={// 核心验证函数
111
+ validateTracking,printValidationResult,// 配置数据(供外部使用)
112
+ TRACKING_METHODS,SDK_TO_TRACKING_MAP,// 工具函数(供外部使用)
113
+ scanSourceFiles,detectDirectTrackingCall,detectSDKCall,hasTrackingImport,hasSDKImport};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/devkit",
3
- "version": "1.0.14",
3
+ "version": "1.0.15-beta.1",
4
4
  "description": "HTML5 Playable Ads 构建工具,支持多广告渠道打包",
5
5
  "main": "core/index.js",
6
6
  "module": "core/index.js",
@@ -15,7 +15,8 @@
15
15
  "defines.d.ts"
16
16
  ],
17
17
  "scripts": {
18
- "deploy": "npm run build && cd dist && npm publish --registry https://registry.npmjs.org/ --access public"
18
+ "deploy": "npm run build && cd dist && npm publish --registry https://registry.npmjs.org/ --access public",
19
+ "deploy:beta": "npm run build && cd dist && npm publish --registry https://registry.npmjs.org/ --access public --tag beta"
19
20
  },
20
21
  "author": "Tencent ADS Team",
21
22
  "license": "MIT",