@playcraft/build 0.0.44 → 0.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base-builder.d.ts +2 -0
- package/dist/base-builder.js +44 -0
- package/dist/engines/playable-scripts-adapter.d.ts +6 -1
- package/dist/engines/playable-scripts-adapter.js +92 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/platforms/bigo.js +3 -5
- package/dist/platforms/inmobi.js +3 -5
- package/dist/platforms/liftoff.js +3 -5
- package/dist/platforms/mintegral.js +4 -6
- package/dist/platforms/snapchat.js +1 -1
- package/dist/platforms/tiktok.js +3 -5
- package/dist/types.d.ts +13 -0
- package/dist/validation/html-validator.d.ts +2 -0
- package/dist/validation/html-validator.js +140 -0
- package/dist/validation/index.d.ts +8 -0
- package/dist/validation/index.js +8 -0
- package/dist/validation/package-validator.d.ts +2 -0
- package/dist/validation/package-validator.js +198 -0
- package/dist/validation/profiles.d.ts +5 -0
- package/dist/validation/profiles.js +222 -0
- package/dist/validation/reporter.d.ts +4 -0
- package/dist/validation/reporter.js +30 -0
- package/dist/validation/source-scanner.d.ts +4 -0
- package/dist/validation/source-scanner.js +271 -0
- package/dist/validation/types.d.ts +112 -0
- package/dist/validation/validation-runner.d.ts +5 -0
- package/dist/validation/validation-runner.js +96 -0
- package/dist/validation/zip-reader.d.ts +17 -0
- package/dist/validation/zip-reader.js +84 -0
- package/dist/vite/platform-configs.js +7 -7
- package/dist/vite-builder.d.ts +1 -0
- package/dist/vite-builder.js +43 -0
- package/package.json +2 -2
- package/dist/analyzers/__tests__/optimization-analyzer.test.js +0 -169
- package/dist/templates/__loading__.js +0 -100
- package/dist/templates/__modules__.js +0 -47
- package/dist/templates/__settings__.template.js +0 -20
- package/dist/templates/__start__.js +0 -332
- package/dist/templates/index.html +0 -18
- package/dist/templates/logo.png +0 -0
- package/dist/templates/manifest.json +0 -1
- package/dist/templates/patches/cannon.min.js +0 -28
- package/dist/templates/patches/lz4.js +0 -10
- package/dist/templates/patches/one-page-http-get.js +0 -20
- package/dist/templates/patches/one-page-inline-game-scripts.js +0 -52
- package/dist/templates/patches/one-page-mraid-resize-canvas.js +0 -46
- package/dist/templates/patches/p2.min.js +0 -27
- package/dist/templates/patches/playcraft-no-xhr.js +0 -76
- package/dist/templates/playcanvas-stable.min.js +0 -16363
- package/dist/templates/styles.css +0 -43
- /package/dist/{analyzers/__tests__/optimization-analyzer.test.d.ts → validation/types.js} +0 -0
package/dist/base-builder.d.ts
CHANGED
package/dist/base-builder.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EngineDetector, PlayCanvasAdapter, GenericAdapter, PlayableScriptsAdapter } from './engines/index.js';
|
|
2
|
+
import { formatValidationSummary, hasBlockingValidationErrors, runPlayableValidation, saveValidationReport, } from './validation/index.js';
|
|
2
3
|
/**
|
|
3
4
|
* 基础构建器 - 路由到不同构建工具适配器
|
|
4
5
|
*
|
|
@@ -33,6 +34,7 @@ export class BaseBuilder {
|
|
|
33
34
|
// 1. 确定引擎类型和构建工具
|
|
34
35
|
const { engine, buildTool } = await this.resolveDetection();
|
|
35
36
|
console.log(`[BaseBuilder] 使用引擎: ${engine}, 构建工具: ${buildTool}`);
|
|
37
|
+
await this.runPrebuildValidation(engine, buildTool);
|
|
36
38
|
// 2. 根据构建工具路由到对应适配器
|
|
37
39
|
let result;
|
|
38
40
|
switch (buildTool) {
|
|
@@ -57,6 +59,48 @@ export class BaseBuilder {
|
|
|
57
59
|
this._metadata = result.metadata;
|
|
58
60
|
return result;
|
|
59
61
|
}
|
|
62
|
+
async runPrebuildValidation(engine, buildTool) {
|
|
63
|
+
const validationOptions = this.options.validation;
|
|
64
|
+
if (validationOptions?.enabled === false || validationOptions?.sourcePrechecked || !validationOptions?.channel) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!this.isPhaserValidationTarget(engine, buildTool)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const requestedStages = validationOptions.stages;
|
|
71
|
+
if (requestedStages && !requestedStages.includes('prebuild-source')) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const stages = ['prebuild-source'];
|
|
75
|
+
const report = await runPlayableValidation({
|
|
76
|
+
projectRoot: this.projectDir,
|
|
77
|
+
channel: validationOptions.channel,
|
|
78
|
+
engine,
|
|
79
|
+
buildTool,
|
|
80
|
+
buildChain: buildTool === 'playable-scripts'
|
|
81
|
+
? 'devkit'
|
|
82
|
+
: buildTool === 'playcanvas-native'
|
|
83
|
+
? 'playcanvas-native'
|
|
84
|
+
: 'generic',
|
|
85
|
+
sourceGlobs: validationOptions.sourceGlobs,
|
|
86
|
+
}, stages);
|
|
87
|
+
const summary = formatValidationSummary(report);
|
|
88
|
+
if (report.summary.status === 'pass') {
|
|
89
|
+
console.log(summary);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.warn(summary);
|
|
93
|
+
}
|
|
94
|
+
if (validationOptions.reportPath) {
|
|
95
|
+
await saveValidationReport(report, validationOptions.reportPath);
|
|
96
|
+
}
|
|
97
|
+
if (validationOptions.failOnError && hasBlockingValidationErrors(report)) {
|
|
98
|
+
throw new Error(`Prebuild validation failed for ${validationOptions.channel}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
isPhaserValidationTarget(engine, buildTool) {
|
|
102
|
+
return engine === 'phaser' || buildTool === 'playable-scripts';
|
|
103
|
+
}
|
|
60
104
|
/**
|
|
61
105
|
* 解析引擎类型和构建工具
|
|
62
106
|
* 优先级:
|
|
@@ -148,13 +148,18 @@ export declare class PlayableScriptsAdapter {
|
|
|
148
148
|
private copyToOutputDir;
|
|
149
149
|
/**
|
|
150
150
|
* 是否对 playable-scripts 使用 --zip。
|
|
151
|
-
*
|
|
151
|
+
* 双格式渠道默认 HTML;仅在请求或配置显式选择 ZIP 时传递 --zip。
|
|
152
152
|
*/
|
|
153
153
|
private isZipOutput;
|
|
154
154
|
/**
|
|
155
155
|
* 获取平台默认的文件名格式
|
|
156
156
|
*/
|
|
157
157
|
private getPlatformFilename;
|
|
158
|
+
private runDevkitOutputValidation;
|
|
159
|
+
private getValidationChannels;
|
|
160
|
+
private isSupportedValidationChannel;
|
|
161
|
+
private pickArtifactForChannel;
|
|
162
|
+
private reportPathForChannel;
|
|
158
163
|
private saveBuildMetadata;
|
|
159
164
|
/**
|
|
160
165
|
* 执行命令并设置超时
|
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { createRequire } from 'module';
|
|
6
|
+
import { formatValidationSummary, hasBlockingValidationErrors, runPlayableValidation, saveValidationReport, } from '../validation/index.js';
|
|
6
7
|
// ESM 兼容:获取当前模块的目录路径
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
9
|
const __dirname = path.dirname(__filename);
|
|
@@ -92,6 +93,7 @@ export class PlayableScriptsAdapter {
|
|
|
92
93
|
const outputs = await this.collectOutputs();
|
|
93
94
|
// 7. 复制产物到 PlayCraft outputDir
|
|
94
95
|
const files = await this.copyToOutputDir(outputs);
|
|
96
|
+
await this.runDevkitOutputValidation(files);
|
|
95
97
|
// 7. 构建元数据
|
|
96
98
|
const metadata = {
|
|
97
99
|
mode: 'classic',
|
|
@@ -258,7 +260,7 @@ export class PlayableScriptsAdapter {
|
|
|
258
260
|
// 输出目录 - 统一使用 dist,与 PlayCraft 标准一致
|
|
259
261
|
const outputDir = config.outputDir ?? 'dist';
|
|
260
262
|
args.push('--out-dir', outputDir);
|
|
261
|
-
// ZIP
|
|
263
|
+
// ZIP 输出:显式 config.zip 或构建请求 format=zip(如 Google / Facebook / TikTok 等选 ZIP)
|
|
262
264
|
const isZipFormat = this.isZipOutput();
|
|
263
265
|
if (isZipFormat) {
|
|
264
266
|
args.push('--zip');
|
|
@@ -1201,25 +1203,13 @@ export class PlayableScriptsAdapter {
|
|
|
1201
1203
|
}
|
|
1202
1204
|
/**
|
|
1203
1205
|
* 是否对 playable-scripts 使用 --zip。
|
|
1204
|
-
*
|
|
1206
|
+
* 双格式渠道默认 HTML;仅在请求或配置显式选择 ZIP 时传递 --zip。
|
|
1205
1207
|
*/
|
|
1206
1208
|
isZipOutput() {
|
|
1207
1209
|
const config = this.config ?? {};
|
|
1208
|
-
const channels = config.channels ?? ['google'];
|
|
1209
1210
|
if (config.zip === true) {
|
|
1210
1211
|
return true;
|
|
1211
1212
|
}
|
|
1212
|
-
const zipOnlyPlatforms = new Set([
|
|
1213
|
-
'tiktok',
|
|
1214
|
-
'snapchat',
|
|
1215
|
-
'liftoff',
|
|
1216
|
-
'bigo',
|
|
1217
|
-
'inmobi',
|
|
1218
|
-
'mintegral',
|
|
1219
|
-
]);
|
|
1220
|
-
if (channels.some((ch) => zipOnlyPlatforms.has(ch))) {
|
|
1221
|
-
return true;
|
|
1222
|
-
}
|
|
1223
1213
|
const fmt = this.options.format;
|
|
1224
1214
|
return fmt === 'zip';
|
|
1225
1215
|
}
|
|
@@ -1248,6 +1238,94 @@ export class PlayableScriptsAdapter {
|
|
|
1248
1238
|
return platformFileNames[channel] || 'index';
|
|
1249
1239
|
}
|
|
1250
1240
|
// ==================== 9. 保存元数据 ====================
|
|
1241
|
+
async runDevkitOutputValidation(files) {
|
|
1242
|
+
const validationOptions = this.options.validation;
|
|
1243
|
+
if (validationOptions?.enabled === false) {
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
const channels = this.getValidationChannels();
|
|
1247
|
+
const artifacts = files.assets.filter((file) => file.endsWith('.html') || file.endsWith('.zip'));
|
|
1248
|
+
if (channels.length === 0 || artifacts.length === 0) {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
const requestedStages = validationOptions?.stages;
|
|
1252
|
+
for (const channel of channels) {
|
|
1253
|
+
const artifact = this.pickArtifactForChannel(channel, artifacts);
|
|
1254
|
+
if (!artifact) {
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
const isZip = artifact.toLowerCase().endsWith('.zip');
|
|
1258
|
+
const stages = requestedStages ?? (isZip ? ['delivery-package'] : ['html-artifact']);
|
|
1259
|
+
const report = await runPlayableValidation({
|
|
1260
|
+
projectRoot: this.projectDir,
|
|
1261
|
+
channel,
|
|
1262
|
+
engine: 'phaser',
|
|
1263
|
+
buildTool: 'playable-scripts',
|
|
1264
|
+
buildChain: 'devkit',
|
|
1265
|
+
htmlPath: isZip ? undefined : artifact,
|
|
1266
|
+
packagePath: isZip ? artifact : undefined,
|
|
1267
|
+
outputDir: this.options.outputDir,
|
|
1268
|
+
sourceGlobs: validationOptions?.sourceGlobs,
|
|
1269
|
+
}, stages);
|
|
1270
|
+
const summary = formatValidationSummary(report);
|
|
1271
|
+
if (report.summary.status === 'pass') {
|
|
1272
|
+
console.log(summary);
|
|
1273
|
+
}
|
|
1274
|
+
else {
|
|
1275
|
+
console.warn(summary);
|
|
1276
|
+
}
|
|
1277
|
+
if (validationOptions?.reportPath) {
|
|
1278
|
+
await saveValidationReport(report, this.reportPathForChannel(validationOptions.reportPath, channel, channels.length));
|
|
1279
|
+
}
|
|
1280
|
+
if (validationOptions?.failOnError && hasBlockingValidationErrors(report)) {
|
|
1281
|
+
throw new Error(`Playable validation failed for ${channel}`);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
getValidationChannels() {
|
|
1286
|
+
if (this.options.validation?.channel) {
|
|
1287
|
+
return [this.options.validation.channel];
|
|
1288
|
+
}
|
|
1289
|
+
const channels = this.config?.channels ?? [];
|
|
1290
|
+
return channels.filter((channel) => this.isSupportedValidationChannel(channel));
|
|
1291
|
+
}
|
|
1292
|
+
isSupportedValidationChannel(channel) {
|
|
1293
|
+
const supported = [
|
|
1294
|
+
'playcraft',
|
|
1295
|
+
'facebook',
|
|
1296
|
+
'snapchat',
|
|
1297
|
+
'ironsource',
|
|
1298
|
+
'applovin',
|
|
1299
|
+
'google',
|
|
1300
|
+
'tiktok',
|
|
1301
|
+
'unity',
|
|
1302
|
+
'liftoff',
|
|
1303
|
+
'moloco',
|
|
1304
|
+
'bigo',
|
|
1305
|
+
'inmobi',
|
|
1306
|
+
'adikteev',
|
|
1307
|
+
'remerge',
|
|
1308
|
+
'mintegral',
|
|
1309
|
+
];
|
|
1310
|
+
return supported.includes(channel);
|
|
1311
|
+
}
|
|
1312
|
+
pickArtifactForChannel(channel, artifacts) {
|
|
1313
|
+
if (artifacts.length === 1) {
|
|
1314
|
+
return artifacts[0];
|
|
1315
|
+
}
|
|
1316
|
+
const direct = artifacts.find((artifact) => {
|
|
1317
|
+
const normalized = artifact.replace(/\\/g, '/').toLowerCase();
|
|
1318
|
+
return normalized.includes(`/${channel}/`) || normalized.includes(`-${channel}.`);
|
|
1319
|
+
});
|
|
1320
|
+
return direct ?? artifacts[0];
|
|
1321
|
+
}
|
|
1322
|
+
reportPathForChannel(reportPath, channel, channelCount) {
|
|
1323
|
+
if (channelCount <= 1) {
|
|
1324
|
+
return reportPath;
|
|
1325
|
+
}
|
|
1326
|
+
const parsed = path.parse(reportPath);
|
|
1327
|
+
return path.join(parsed.dir, `${parsed.name}.${channel}${parsed.ext || '.json'}`);
|
|
1328
|
+
}
|
|
1251
1329
|
async saveBuildMetadata(metadata) {
|
|
1252
1330
|
// 不再保存 .build-metadata.json 文件,元数据通过返回值传递
|
|
1253
1331
|
console.log(`[PlayableScriptsAdapter] 构建完成(buildTool: playable-scripts, skipChannelBuild: true)`);
|
package/dist/index.d.ts
CHANGED
|
@@ -25,4 +25,5 @@ export { PLATFORM_CONFIGS } from './vite/platform-configs.js';
|
|
|
25
25
|
export type { PlatformViteConfig } from './vite/platform-configs.js';
|
|
26
26
|
export { AudioOptimizer } from './audio-optimizer.js';
|
|
27
27
|
export type { AudioAssetInfo, AudioOptimizationReport, AudioOptimizerOptions } from './audio-optimizer.js';
|
|
28
|
+
export * from './validation/index.js';
|
|
28
29
|
export * from './types.js';
|
package/dist/index.js
CHANGED
package/dist/platforms/bigo.js
CHANGED
|
@@ -8,7 +8,7 @@ export class BigoAdapter extends PlatformAdapter {
|
|
|
8
8
|
return 5 * 1024 * 1024;
|
|
9
9
|
}
|
|
10
10
|
getDefaultFormat() {
|
|
11
|
-
return '
|
|
11
|
+
return 'html';
|
|
12
12
|
}
|
|
13
13
|
async modifyHTML(html, assets) {
|
|
14
14
|
// BIGO Ads 需要 BIGO JS-SDK
|
|
@@ -36,7 +36,7 @@ export class BigoAdapter extends PlatformAdapter {
|
|
|
36
36
|
`;
|
|
37
37
|
const sdkScript = `<script src="https://static-web.likeevideo.com/oss/material-ad/playable-ad-demo/bgy-mraid-sdk.js"></script>`;
|
|
38
38
|
const fallbackScript = await this.minifyPlatformScript(bigoScriptCode, 'bigo-sdk');
|
|
39
|
-
const comment = `<!-- BIGO: ZIP
|
|
39
|
+
const comment = `<!-- BIGO: HTML/ZIP format, config.json includes orientation when present, use BGY_MRAID.open() -->`;
|
|
40
40
|
// 在 </head> 之前插入
|
|
41
41
|
html = html.replace('</head>', `${sdkScript}${fallbackScript}${comment}</head>`);
|
|
42
42
|
// 注入统一的 CTA 适配器
|
|
@@ -71,8 +71,6 @@ export class BigoAdapter extends PlatformAdapter {
|
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
73
|
validateOptions() {
|
|
74
|
-
|
|
75
|
-
console.warn('警告: BIGO Ads 要求 ZIP 格式');
|
|
76
|
-
}
|
|
74
|
+
// BIGO Ads supports both HTML and ZIP output in PlayCraft.
|
|
77
75
|
}
|
|
78
76
|
}
|
package/dist/platforms/inmobi.js
CHANGED
|
@@ -8,7 +8,7 @@ export class InMobiAdapter extends PlatformAdapter {
|
|
|
8
8
|
return 5 * 1024 * 1024;
|
|
9
9
|
}
|
|
10
10
|
getDefaultFormat() {
|
|
11
|
-
return '
|
|
11
|
+
return 'html';
|
|
12
12
|
}
|
|
13
13
|
async modifyHTML(html, assets) {
|
|
14
14
|
// InMobi 需要 MRAID 支持
|
|
@@ -36,7 +36,7 @@ export class InMobiAdapter extends PlatformAdapter {
|
|
|
36
36
|
};
|
|
37
37
|
`;
|
|
38
38
|
const inmobiScript = await this.minifyPlatformScript(inmobiScriptCode, 'inmobi-mraid');
|
|
39
|
-
const comment = `<!-- InMobi: ZIP
|
|
39
|
+
const comment = `<!-- InMobi: HTML/ZIP format, use mraid.open() -->`;
|
|
40
40
|
// 在 </head> 之前插入平台特定的 API
|
|
41
41
|
html = html.replace('</head>', `${inmobiScript}${comment}</head>`);
|
|
42
42
|
// 注入统一的 CTA 适配器
|
|
@@ -57,8 +57,6 @@ export class InMobiAdapter extends PlatformAdapter {
|
|
|
57
57
|
`;
|
|
58
58
|
}
|
|
59
59
|
validateOptions() {
|
|
60
|
-
|
|
61
|
-
console.warn('警告: InMobi 要求 ZIP 格式');
|
|
62
|
-
}
|
|
60
|
+
// InMobi supports both HTML and ZIP output in PlayCraft.
|
|
63
61
|
}
|
|
64
62
|
}
|
|
@@ -8,7 +8,7 @@ export class LiftoffAdapter extends PlatformAdapter {
|
|
|
8
8
|
return 5 * 1024 * 1024;
|
|
9
9
|
}
|
|
10
10
|
getDefaultFormat() {
|
|
11
|
-
return '
|
|
11
|
+
return 'html';
|
|
12
12
|
}
|
|
13
13
|
async modifyHTML(html, assets) {
|
|
14
14
|
// Liftoff 支持 MRAID 或 window.open
|
|
@@ -31,7 +31,7 @@ export class LiftoffAdapter extends PlatformAdapter {
|
|
|
31
31
|
};
|
|
32
32
|
`;
|
|
33
33
|
const liftoffScript = await this.minifyPlatformScript(liftoffScriptCode, 'liftoff-mraid');
|
|
34
|
-
const comment = `<!-- Liftoff: ZIP
|
|
34
|
+
const comment = `<!-- Liftoff: HTML/ZIP format, ASCII filenames recommended, use mraid.open() or window.open() -->`;
|
|
35
35
|
// 在 </head> 之前插入
|
|
36
36
|
html = html.replace('</head>', `${liftoffScript}${comment}</head>`);
|
|
37
37
|
// 注入统一的 CTA 适配器
|
|
@@ -47,8 +47,6 @@ export class LiftoffAdapter extends PlatformAdapter {
|
|
|
47
47
|
`;
|
|
48
48
|
}
|
|
49
49
|
validateOptions() {
|
|
50
|
-
|
|
51
|
-
console.warn('警告: Liftoff 要求 ZIP 格式');
|
|
52
|
-
}
|
|
50
|
+
// Liftoff supports both HTML and ZIP output in PlayCraft.
|
|
53
51
|
}
|
|
54
52
|
}
|
|
@@ -4,11 +4,11 @@ export class MintegralAdapter extends PlatformAdapter {
|
|
|
4
4
|
return 'Mintegral';
|
|
5
5
|
}
|
|
6
6
|
getSizeLimit() {
|
|
7
|
-
// Mintegral: 5MB
|
|
7
|
+
// Mintegral: 5MB
|
|
8
8
|
return 5 * 1024 * 1024;
|
|
9
9
|
}
|
|
10
10
|
getDefaultFormat() {
|
|
11
|
-
return '
|
|
11
|
+
return 'html';
|
|
12
12
|
}
|
|
13
13
|
async modifyHTML(html, assets) {
|
|
14
14
|
// Mintegral 要求 charset 为 utf-8,确保存在
|
|
@@ -33,7 +33,7 @@ export class MintegralAdapter extends PlatformAdapter {
|
|
|
33
33
|
window.HttpAPI = window.HttpAPI || { sendPoint: function() {} };
|
|
34
34
|
`;
|
|
35
35
|
const mintegralScript = await this.minifyPlatformScript(mintegralScriptCode, 'mintegral-sdk');
|
|
36
|
-
const comment = `<!-- Mintegral: ZIP
|
|
36
|
+
const comment = `<!-- Mintegral: HTML/ZIP format, 5MB limit, use window.install(), supports both orientations -->`;
|
|
37
37
|
// 在 </head> 之前插入 polyfill 和注释,如果没有 </head> 则在文件开头插入
|
|
38
38
|
if (html.includes('</head>')) {
|
|
39
39
|
html = html.replace('</head>', `${mintegralScript}${comment}\n</head>`);
|
|
@@ -59,8 +59,6 @@ export class MintegralAdapter extends PlatformAdapter {
|
|
|
59
59
|
`;
|
|
60
60
|
}
|
|
61
61
|
validateOptions() {
|
|
62
|
-
|
|
63
|
-
console.warn('警告: Mintegral 要求 ZIP 格式,将自动使用 ZIP');
|
|
64
|
-
}
|
|
62
|
+
// Mintegral supports both HTML and ZIP output in PlayCraft.
|
|
65
63
|
}
|
|
66
64
|
}
|
package/dist/platforms/tiktok.js
CHANGED
|
@@ -8,7 +8,7 @@ export class TikTokAdapter extends PlatformAdapter {
|
|
|
8
8
|
return 5 * 1024 * 1024;
|
|
9
9
|
}
|
|
10
10
|
getDefaultFormat() {
|
|
11
|
-
return '
|
|
11
|
+
return 'html';
|
|
12
12
|
}
|
|
13
13
|
async modifyHTML(html, assets) {
|
|
14
14
|
// TikTok/Pangle 需要 Pangle JS-SDK
|
|
@@ -35,7 +35,7 @@ export class TikTokAdapter extends PlatformAdapter {
|
|
|
35
35
|
};
|
|
36
36
|
`;
|
|
37
37
|
const pangleScript = await this.minifyPlatformScript(pangleScriptCode, 'pangle-sdk');
|
|
38
|
-
const comment = `<!-- TikTok/Pangle: ZIP
|
|
38
|
+
const comment = `<!-- TikTok/Pangle: HTML/ZIP format, config.json includes playable_orientation when present, no mraid.js -->`;
|
|
39
39
|
// 在 </head> 之前插入
|
|
40
40
|
html = html.replace('</head>', `${pangleScript}${comment}</head>`);
|
|
41
41
|
// 注入统一的 CTA 适配器
|
|
@@ -63,8 +63,6 @@ export class TikTokAdapter extends PlatformAdapter {
|
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
validateOptions() {
|
|
66
|
-
|
|
67
|
-
console.warn('警告: TikTok/Pangle 要求 ZIP 格式');
|
|
68
|
-
}
|
|
66
|
+
// TikTok/Pangle supports both HTML and ZIP output in PlayCraft.
|
|
69
67
|
}
|
|
70
68
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -7,6 +7,16 @@ export interface StoreUrls {
|
|
|
7
7
|
ios?: string;
|
|
8
8
|
android?: string;
|
|
9
9
|
}
|
|
10
|
+
export interface ComplianceValidationOptions {
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
failOnError?: boolean;
|
|
13
|
+
channel?: Platform;
|
|
14
|
+
reportPath?: string;
|
|
15
|
+
sourcePrechecked?: boolean;
|
|
16
|
+
stages?: Array<'prebuild-source' | 'base-output' | 'html-artifact' | 'delivery-package' | 'runtime-manual' | 'external-validator'>;
|
|
17
|
+
sourceGlobs?: string[];
|
|
18
|
+
sourceDebug?: unknown;
|
|
19
|
+
}
|
|
10
20
|
export type BuildMode = 'classic' | 'esm';
|
|
11
21
|
export type ESMMode = 'auto' | 'enabled' | 'disabled';
|
|
12
22
|
export interface BuildOptions {
|
|
@@ -48,6 +58,7 @@ export interface BuildOptions {
|
|
|
48
58
|
preserveESM?: boolean;
|
|
49
59
|
forceIIFE?: boolean;
|
|
50
60
|
storeUrls?: StoreUrls;
|
|
61
|
+
validation?: ComplianceValidationOptions;
|
|
51
62
|
tracking?: {
|
|
52
63
|
enabled?: boolean;
|
|
53
64
|
adNetwork?: string;
|
|
@@ -208,6 +219,7 @@ export interface BaseBuildOptions {
|
|
|
208
219
|
engine?: EngineType;
|
|
209
220
|
/** 渠道产物格式(playable-scripts 阶段;与 Vite 阶段 format 对齐) */
|
|
210
221
|
format?: OutputFormat;
|
|
222
|
+
validation?: ComplianceValidationOptions;
|
|
211
223
|
}
|
|
212
224
|
/**
|
|
213
225
|
* 基础构建输出
|
|
@@ -268,4 +280,5 @@ export interface BuildOptions {
|
|
|
268
280
|
forceIIFE?: boolean;
|
|
269
281
|
engine?: EngineType;
|
|
270
282
|
storeUrls?: StoreUrls;
|
|
283
|
+
validation?: ComplianceValidationOptions;
|
|
271
284
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { getRulesForStage } from './profiles.js';
|
|
3
|
+
const RESOURCE_ATTR_RE = /\b(?:src|href|data)\s*=\s*["']([^"']+)["']/gi;
|
|
4
|
+
const CSS_URL_RE = /url\(\s*["']?([^"')]+)["']?\s*\)/gi;
|
|
5
|
+
export async function validateHtmlArtifact(context, profile) {
|
|
6
|
+
const rules = profile?.rules.filter((rule) => rule.stage === 'html-artifact')
|
|
7
|
+
?? getRulesForStage(context.channel, 'html-artifact');
|
|
8
|
+
if (!context.htmlPath) {
|
|
9
|
+
return [missingHtmlResult(context.channel)];
|
|
10
|
+
}
|
|
11
|
+
let artifact;
|
|
12
|
+
try {
|
|
13
|
+
const [content, stat] = await Promise.all([
|
|
14
|
+
fs.readFile(context.htmlPath, 'utf-8'),
|
|
15
|
+
fs.stat(context.htmlPath),
|
|
16
|
+
]);
|
|
17
|
+
artifact = {
|
|
18
|
+
path: context.htmlPath,
|
|
19
|
+
content,
|
|
20
|
+
size: stat.size,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return [missingHtmlResult(context.channel, context.htmlPath, error)];
|
|
25
|
+
}
|
|
26
|
+
const results = [];
|
|
27
|
+
for (const rule of rules) {
|
|
28
|
+
if (!rule.detectable || !rule.check) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
results.push(evaluateHtmlRule(rule, artifact));
|
|
32
|
+
}
|
|
33
|
+
return results;
|
|
34
|
+
}
|
|
35
|
+
function missingHtmlResult(channel, htmlPath, error) {
|
|
36
|
+
return {
|
|
37
|
+
ruleId: `${channel}.html.entry.exists`,
|
|
38
|
+
channel,
|
|
39
|
+
stage: 'html-artifact',
|
|
40
|
+
severity: 'error',
|
|
41
|
+
status: 'fail',
|
|
42
|
+
message: htmlPath
|
|
43
|
+
? `HTML artifact is not readable: ${htmlPath}`
|
|
44
|
+
: 'HTML artifact path is missing.',
|
|
45
|
+
recommendation: 'Pass the final channel HTML artifact path to the validation context.',
|
|
46
|
+
file: htmlPath,
|
|
47
|
+
evidence: error instanceof Error ? error.message : undefined,
|
|
48
|
+
owner: 'playcraft-build',
|
|
49
|
+
confidence: 'high',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function evaluateHtmlRule(rule, artifact) {
|
|
53
|
+
const check = rule.check;
|
|
54
|
+
switch (check.kind) {
|
|
55
|
+
case 'max-size':
|
|
56
|
+
return resultFromBoolean(rule, artifact.size <= check.maxBytes, artifact.path, `actual=${formatBytes(artifact.size)}, limit=${formatBytes(check.maxBytes)}`);
|
|
57
|
+
case 'external-resources': {
|
|
58
|
+
const externalResources = collectExternalResources(artifact.content)
|
|
59
|
+
.filter((url) => !isAllowedExternalResource(url, check.allowedPatterns));
|
|
60
|
+
return resultFromBoolean(rule, externalResources.length === 0, artifact.path, externalResources.slice(0, 20).join(', '));
|
|
61
|
+
}
|
|
62
|
+
case 'required-pattern': {
|
|
63
|
+
const regex = new RegExp(check.pattern, check.flags);
|
|
64
|
+
return resultFromBoolean(rule, regex.test(artifact.content), artifact.path, check.evidence ?? check.pattern);
|
|
65
|
+
}
|
|
66
|
+
case 'forbidden-pattern': {
|
|
67
|
+
const regex = new RegExp(check.pattern, check.flags);
|
|
68
|
+
const match = artifact.content.match(regex);
|
|
69
|
+
return resultFromBoolean(rule, !match, artifact.path, match?.[0] ?? check.evidence ?? check.pattern);
|
|
70
|
+
}
|
|
71
|
+
case 'single-html':
|
|
72
|
+
return resultFromBoolean(rule, collectLocalResourceReferences(artifact.content).length === 0, artifact.path, collectLocalResourceReferences(artifact.content).slice(0, 20).join(', '));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function resultFromBoolean(rule, passed, file, evidence) {
|
|
76
|
+
return {
|
|
77
|
+
ruleId: rule.id,
|
|
78
|
+
channel: rule.channel,
|
|
79
|
+
stage: rule.stage,
|
|
80
|
+
severity: rule.severity,
|
|
81
|
+
status: passed ? 'pass' : (rule.severity === 'error' ? 'fail' : 'warning'),
|
|
82
|
+
message: passed ? `${rule.title}: passed` : rule.title,
|
|
83
|
+
recommendation: rule.recommendation,
|
|
84
|
+
file,
|
|
85
|
+
evidence: evidence || undefined,
|
|
86
|
+
owner: rule.owner,
|
|
87
|
+
confidence: rule.confidence,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function collectExternalResources(html) {
|
|
91
|
+
return collectUrls(html).filter((url) => /^https?:\/\//i.test(url) || /^\/\//.test(url));
|
|
92
|
+
}
|
|
93
|
+
function collectLocalResourceReferences(html) {
|
|
94
|
+
return collectUrls(html).filter((url) => {
|
|
95
|
+
if (isInlineOrSpecialUrl(url))
|
|
96
|
+
return false;
|
|
97
|
+
if (/^https?:\/\//i.test(url) || /^\/\//.test(url))
|
|
98
|
+
return false;
|
|
99
|
+
return !url.startsWith('#');
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function collectUrls(html) {
|
|
103
|
+
const urls = new Set();
|
|
104
|
+
for (const match of html.matchAll(RESOURCE_ATTR_RE)) {
|
|
105
|
+
if (match[1])
|
|
106
|
+
urls.add(match[1].trim());
|
|
107
|
+
}
|
|
108
|
+
for (const match of html.matchAll(CSS_URL_RE)) {
|
|
109
|
+
if (match[1])
|
|
110
|
+
urls.add(match[1].trim());
|
|
111
|
+
}
|
|
112
|
+
return Array.from(urls);
|
|
113
|
+
}
|
|
114
|
+
function isInlineOrSpecialUrl(url) {
|
|
115
|
+
return /^(data:|blob:|javascript:|mailto:|tel:|about:)/i.test(url);
|
|
116
|
+
}
|
|
117
|
+
function isAllowedExternalResource(url, allowedPatterns) {
|
|
118
|
+
if (!allowedPatterns || allowedPatterns.length === 0) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return allowedPatterns.some((pattern) => {
|
|
122
|
+
if (url.includes(pattern))
|
|
123
|
+
return true;
|
|
124
|
+
try {
|
|
125
|
+
return new RegExp(pattern, 'i').test(url);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function formatBytes(bytes) {
|
|
133
|
+
if (!Number.isFinite(bytes))
|
|
134
|
+
return String(bytes);
|
|
135
|
+
const mb = bytes / 1024 / 1024;
|
|
136
|
+
if (mb >= 1)
|
|
137
|
+
return `${mb.toFixed(2)} MB`;
|
|
138
|
+
const kb = bytes / 1024;
|
|
139
|
+
return `${kb.toFixed(1)} KB`;
|
|
140
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './profiles.js';
|
|
3
|
+
export * from './html-validator.js';
|
|
4
|
+
export * from './package-validator.js';
|
|
5
|
+
export * from './reporter.js';
|
|
6
|
+
export * from './source-scanner.js';
|
|
7
|
+
export * from './validation-runner.js';
|
|
8
|
+
export * from './zip-reader.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './profiles.js';
|
|
3
|
+
export * from './html-validator.js';
|
|
4
|
+
export * from './package-validator.js';
|
|
5
|
+
export * from './reporter.js';
|
|
6
|
+
export * from './source-scanner.js';
|
|
7
|
+
export * from './validation-runner.js';
|
|
8
|
+
export * from './zip-reader.js';
|