@playcraft/build 0.0.57 → 0.0.58
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/README.md +2 -2
- package/dist/engines/playable-scripts-adapter.d.ts +27 -0
- package/dist/engines/playable-scripts-adapter.js +91 -3
- package/dist/platforms/facebook.js +3 -1
- package/dist/platforms/remerge.js +6 -4
- package/dist/validation/profiles.js +2 -2
- package/dist/vite/platform-configs.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -177,7 +177,7 @@ const adapter = createPlatformAdapter({
|
|
|
177
177
|
});
|
|
178
178
|
|
|
179
179
|
console.log(adapter.getName()); // "Facebook"
|
|
180
|
-
console.log(adapter.getSizeLimit()); //
|
|
180
|
+
console.log(adapter.getSizeLimit()); // 5242880 (5MB)
|
|
181
181
|
console.log(adapter.getDefaultFormat()); // "html"
|
|
182
182
|
```
|
|
183
183
|
|
|
@@ -209,4 +209,4 @@ console.log(adapter.getDefaultFormat()); // "html"
|
|
|
209
209
|
|
|
210
210
|
- `@playcraft/cli`:CLI 工具
|
|
211
211
|
- `@playcraft/worker`:队列消费与异步构建
|
|
212
|
-
- `@playcraft/backend`:类型定义用于参数校验
|
|
212
|
+
- `@playcraft/backend-business-server`:类型定义用于参数校验
|
|
@@ -153,8 +153,18 @@ export declare class PlayableScriptsAdapter {
|
|
|
153
153
|
/**
|
|
154
154
|
* 是否对 playable-scripts 使用 --zip。
|
|
155
155
|
* 双格式渠道默认 HTML;仅在请求或配置显式选择 ZIP 时传递 --zip。
|
|
156
|
+
*
|
|
157
|
+
* 例外:Facebook 在 devkit(Phaser / playable-scripts)路径下产物固定为 ZIP,
|
|
158
|
+
* 因此无论请求的 format 一律传递 --zip。
|
|
159
|
+
* 该规则只作用于 devkit 路径,PlayCanvas 渠道构建不受影响。
|
|
156
160
|
*/
|
|
157
161
|
private isZipOutput;
|
|
162
|
+
/**
|
|
163
|
+
* 是否为 Facebook 渠道的 devkit 构建
|
|
164
|
+
* (devkit = Phaser / playable-scripts 路径,仅该路径会走到本适配器;
|
|
165
|
+
* 渠道来源与产物校验保持一致:validation.channel 优先,其次 playableScriptsConfig.channels)
|
|
166
|
+
*/
|
|
167
|
+
private isFacebookDevkitChannel;
|
|
158
168
|
/**
|
|
159
169
|
* 获取平台默认的文件名格式
|
|
160
170
|
*/
|
|
@@ -165,6 +175,23 @@ export declare class PlayableScriptsAdapter {
|
|
|
165
175
|
* 普通构建链由对应 PlatformAdapter 处理。
|
|
166
176
|
*/
|
|
167
177
|
private applyChannelLifecycleBridges;
|
|
178
|
+
/**
|
|
179
|
+
* 兜底:渠道要求 ZIP,但第三方(devkit)只产出了单文件 HTML 时,现场把 HTML 打成
|
|
180
|
+
* `playable.zip`(根目录 `index.html`),并删除原始 HTML。
|
|
181
|
+
*
|
|
182
|
+
* 为什么必须在适配器里做:下游(沙箱 runner / worker)是**按产物后缀**决定
|
|
183
|
+
* manifest.format 的,留有 .html 就一定会被判成 html;只有让 outputDir 里只剩
|
|
184
|
+
* `playable.zip`,format 才会正确地是 zip。
|
|
185
|
+
*/
|
|
186
|
+
private ensureZipFallbackArtifacts;
|
|
187
|
+
/**
|
|
188
|
+
* 从产物路径反推渠道:
|
|
189
|
+
* 1. 单渠道构建:产物直接落在 outputDir 下,取 `config.channels[0]`
|
|
190
|
+
* 2. 批量构建:路径形如 `outputDir/<channel>/<file>`,按目录名(含渠道映射名)匹配
|
|
191
|
+
* 3. 退化:`options.validation.channel`(沙箱 runner / worker / CLI 都会传本次渠道),
|
|
192
|
+
* 保证 `channels` 缺失时兜底不会静默失效
|
|
193
|
+
*/
|
|
194
|
+
private resolveArtifactChannel;
|
|
168
195
|
private getValidationChannels;
|
|
169
196
|
private isSupportedValidationChannel;
|
|
170
197
|
private pickArtifactForChannel;
|
|
@@ -20,15 +20,30 @@ const CHANNEL_MAP = {
|
|
|
20
20
|
playcraft: 'preview',
|
|
21
21
|
};
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
23
|
+
* devkit(playable-scripts)不支持的渠道——这些渠道会被从 `--channels` 中剔除。
|
|
24
|
+
*
|
|
25
|
+
* 目前为空:devkit 已覆盖 PlayCraft 全部渠道(含 remerge,产物为 ZIP)。
|
|
26
|
+
* 若将来新增 PlayCraft 有、devkit 无的渠道,在此登记即可恢复过滤行为。
|
|
24
27
|
*/
|
|
25
|
-
const UNSUPPORTED_CHANNELS = new Set([
|
|
28
|
+
const UNSUPPORTED_CHANNELS = new Set([]);
|
|
26
29
|
/**
|
|
27
30
|
* 将 PlayCraft 平台名映射为 playable-scripts 渠道名
|
|
28
31
|
*/
|
|
29
32
|
function mapChannel(playcraftPlatform) {
|
|
30
33
|
return CHANNEL_MAP[playcraftPlatform] ?? playcraftPlatform;
|
|
31
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* 需要「HTML → ZIP」兜底的渠道:交付与后端校验(PlatformValidatorService)都要求 ZIP,
|
|
37
|
+
* 但 devkit(第三方)可能只产出单文件 HTML。
|
|
38
|
+
*
|
|
39
|
+
* 背景:产物格式由下游按**产物后缀**判定(runner.cjs / worker main.js 的 devkit 分支),
|
|
40
|
+
* 一旦 devkit 漏出 .html,就会与「remerge 必须 ZIP」的硬规则冲突,报
|
|
41
|
+
* `平台要求 ZIP 格式,但输出为 HTML`(FORMAT_MISMATCH)。
|
|
42
|
+
*
|
|
43
|
+
* 注意:bigo / snapchat 不在此列 —— 它们的 HTML→ZIP 兜底在 runner / worker 的
|
|
44
|
+
* htmlBackedZip 分支完成(还要额外注入 config.json 的 orientation),不能在这里简单打包。
|
|
45
|
+
*/
|
|
46
|
+
const ZIP_FALLBACK_CHANNELS = new Set(['remerge']);
|
|
32
47
|
/**
|
|
33
48
|
* PlayableScripts 构建工具适配器
|
|
34
49
|
*
|
|
@@ -132,7 +147,10 @@ export class PlayableScriptsAdapter {
|
|
|
132
147
|
// 7. 复制产物到 PlayCraft outputDir
|
|
133
148
|
const files = await this.copyToOutputDir(outputs);
|
|
134
149
|
await this.applyChannelLifecycleBridges(files);
|
|
150
|
+
// 校验放在兜底打包之前:渠道规则(size / external-resource 等)都在 html-artifact 阶段,
|
|
151
|
+
// 若先打成 ZIP,校验会因产物是 ZIP 而跳过这些规则,等于丢掉校验覆盖。
|
|
135
152
|
await this.runDevkitOutputValidation(files);
|
|
153
|
+
await this.ensureZipFallbackArtifacts(files);
|
|
136
154
|
// 7. 构建元数据
|
|
137
155
|
const metadata = {
|
|
138
156
|
mode: 'classic',
|
|
@@ -301,7 +319,7 @@ export class PlayableScriptsAdapter {
|
|
|
301
319
|
// 输出目录 - 统一使用 dist,与 PlayCraft 标准一致
|
|
302
320
|
const outputDir = config.outputDir ?? 'dist';
|
|
303
321
|
args.push('--out-dir', outputDir);
|
|
304
|
-
// ZIP 输出:显式 config.zip
|
|
322
|
+
// ZIP 输出:显式 config.zip、Facebook(devkit 下恒为 ZIP)或构建请求 format=zip
|
|
305
323
|
const isZipFormat = this.isZipOutput();
|
|
306
324
|
if (isZipFormat) {
|
|
307
325
|
args.push('--zip');
|
|
@@ -1245,15 +1263,30 @@ export class PlayableScriptsAdapter {
|
|
|
1245
1263
|
/**
|
|
1246
1264
|
* 是否对 playable-scripts 使用 --zip。
|
|
1247
1265
|
* 双格式渠道默认 HTML;仅在请求或配置显式选择 ZIP 时传递 --zip。
|
|
1266
|
+
*
|
|
1267
|
+
* 例外:Facebook 在 devkit(Phaser / playable-scripts)路径下产物固定为 ZIP,
|
|
1268
|
+
* 因此无论请求的 format 一律传递 --zip。
|
|
1269
|
+
* 该规则只作用于 devkit 路径,PlayCanvas 渠道构建不受影响。
|
|
1248
1270
|
*/
|
|
1249
1271
|
isZipOutput() {
|
|
1250
1272
|
const config = this.config ?? {};
|
|
1251
1273
|
if (config.zip === true) {
|
|
1252
1274
|
return true;
|
|
1253
1275
|
}
|
|
1276
|
+
if (this.isFacebookDevkitChannel()) {
|
|
1277
|
+
return true;
|
|
1278
|
+
}
|
|
1254
1279
|
const fmt = this.options.format;
|
|
1255
1280
|
return fmt === 'zip';
|
|
1256
1281
|
}
|
|
1282
|
+
/**
|
|
1283
|
+
* 是否为 Facebook 渠道的 devkit 构建
|
|
1284
|
+
* (devkit = Phaser / playable-scripts 路径,仅该路径会走到本适配器;
|
|
1285
|
+
* 渠道来源与产物校验保持一致:validation.channel 优先,其次 playableScriptsConfig.channels)
|
|
1286
|
+
*/
|
|
1287
|
+
isFacebookDevkitChannel() {
|
|
1288
|
+
return this.getValidationChannels().some((channel) => mapChannel(channel) === 'facebook');
|
|
1289
|
+
}
|
|
1257
1290
|
/**
|
|
1258
1291
|
* 获取平台默认的文件名格式
|
|
1259
1292
|
*/
|
|
@@ -1351,6 +1384,61 @@ export class PlayableScriptsAdapter {
|
|
|
1351
1384
|
archive[htmlEntry] = strToU8(injectBigoLifecycleBridge(strFromU8(archive[htmlEntry])));
|
|
1352
1385
|
await fs.writeFile(artifact, zipSync(archive, { level: 9 }));
|
|
1353
1386
|
}
|
|
1387
|
+
/**
|
|
1388
|
+
* 兜底:渠道要求 ZIP,但第三方(devkit)只产出了单文件 HTML 时,现场把 HTML 打成
|
|
1389
|
+
* `playable.zip`(根目录 `index.html`),并删除原始 HTML。
|
|
1390
|
+
*
|
|
1391
|
+
* 为什么必须在适配器里做:下游(沙箱 runner / worker)是**按产物后缀**决定
|
|
1392
|
+
* manifest.format 的,留有 .html 就一定会被判成 html;只有让 outputDir 里只剩
|
|
1393
|
+
* `playable.zip`,format 才会正确地是 zip。
|
|
1394
|
+
*/
|
|
1395
|
+
async ensureZipFallbackArtifacts(files) {
|
|
1396
|
+
const htmlArtifacts = files.assets.filter((artifact) => artifact.toLowerCase().endsWith('.html'));
|
|
1397
|
+
if (htmlArtifacts.length === 0) {
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
for (const htmlPath of htmlArtifacts) {
|
|
1401
|
+
const channel = this.resolveArtifactChannel(htmlPath);
|
|
1402
|
+
if (!channel || !ZIP_FALLBACK_CHANNELS.has(channel)) {
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
const zipPath = path.join(path.dirname(htmlPath), 'playable.zip');
|
|
1406
|
+
const htmlBuffer = await fs.readFile(htmlPath);
|
|
1407
|
+
await fs.writeFile(zipPath, zipSync({ 'index.html': new Uint8Array(htmlBuffer) }, { level: 9 }));
|
|
1408
|
+
await fs.unlink(htmlPath);
|
|
1409
|
+
files.assets = files.assets.filter((artifact) => artifact !== htmlPath);
|
|
1410
|
+
files.assets.push(zipPath);
|
|
1411
|
+
if (files.html === htmlPath) {
|
|
1412
|
+
files.html = '';
|
|
1413
|
+
}
|
|
1414
|
+
console.log(`[PlayableScriptsAdapter] ${channel} 交付要求 ZIP,但 devkit 产出单文件 HTML;` +
|
|
1415
|
+
`已兜底打包: ${path.relative(this.projectDir, zipPath)}(原始 HTML 已移除)`);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* 从产物路径反推渠道:
|
|
1420
|
+
* 1. 单渠道构建:产物直接落在 outputDir 下,取 `config.channels[0]`
|
|
1421
|
+
* 2. 批量构建:路径形如 `outputDir/<channel>/<file>`,按目录名(含渠道映射名)匹配
|
|
1422
|
+
* 3. 退化:`options.validation.channel`(沙箱 runner / worker / CLI 都会传本次渠道),
|
|
1423
|
+
* 保证 `channels` 缺失时兜底不会静默失效
|
|
1424
|
+
*/
|
|
1425
|
+
resolveArtifactChannel(artifactPath) {
|
|
1426
|
+
const channels = this.config?.channels ?? [];
|
|
1427
|
+
if (channels.length === 1) {
|
|
1428
|
+
return channels[0];
|
|
1429
|
+
}
|
|
1430
|
+
if (channels.length > 1) {
|
|
1431
|
+
const normalized = artifactPath.replace(/\\/g, '/').toLowerCase();
|
|
1432
|
+
const matched = channels.find((channel) => {
|
|
1433
|
+
const mapped = mapChannel(channel).toLowerCase();
|
|
1434
|
+
return (normalized.includes(`/${channel.toLowerCase()}/`) || normalized.includes(`/${mapped}/`));
|
|
1435
|
+
});
|
|
1436
|
+
if (matched) {
|
|
1437
|
+
return matched;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return this.options.validation?.channel;
|
|
1441
|
+
}
|
|
1354
1442
|
getValidationChannels() {
|
|
1355
1443
|
if (this.options.validation?.channel) {
|
|
1356
1444
|
return [this.options.validation.channel];
|
|
@@ -4,10 +4,12 @@ export class FacebookAdapter extends PlatformAdapter {
|
|
|
4
4
|
return 'Facebook';
|
|
5
5
|
}
|
|
6
6
|
getSizeLimit() {
|
|
7
|
-
// Facebook:
|
|
7
|
+
// Facebook: 制品上限 5MB(HTML / ZIP 均为 5MB;ZIP 内的 index.html 不单独限制)
|
|
8
8
|
return 5 * 1024 * 1024;
|
|
9
9
|
}
|
|
10
10
|
getDefaultFormat() {
|
|
11
|
+
// 渠道默认产物为单文件 HTML;Phaser(playable-scripts devkit)路径固定输出 ZIP,
|
|
12
|
+
// 由 devkit 输入侧的打包配置决定,见 playable-scripts-adapter.ts 的 isZipOutput()。
|
|
11
13
|
return 'html';
|
|
12
14
|
}
|
|
13
15
|
async modifyHTML(html, assets) {
|
|
@@ -8,7 +8,9 @@ export class RemergeAdapter extends PlatformAdapter {
|
|
|
8
8
|
return 5 * 1024 * 1024;
|
|
9
9
|
}
|
|
10
10
|
getDefaultFormat() {
|
|
11
|
-
|
|
11
|
+
// Remerge 交付 ZIP(devkit 与 Vite 两条路径一致),与后端
|
|
12
|
+
// PlatformValidatorService / phaser-size-estimate.model 的渠道格式表保持一致。
|
|
13
|
+
return 'zip';
|
|
12
14
|
}
|
|
13
15
|
async modifyHTML(html, assets) {
|
|
14
16
|
// Remerge 使用 FbPlayableAd API(与 Facebook/Moloco 相同)
|
|
@@ -29,7 +31,7 @@ export class RemergeAdapter extends PlatformAdapter {
|
|
|
29
31
|
`;
|
|
30
32
|
const remergeScript = await this.minifyPlatformScript(remergeScriptCode, 'remerge-api');
|
|
31
33
|
const meta = `<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">`;
|
|
32
|
-
const comment = `<!-- Remerge:
|
|
34
|
+
const comment = `<!-- Remerge: ZIP 交付包 <5MB, CTA 无参数调用 -->`;
|
|
33
35
|
// 在 </head> 之前插入平台特定的 API
|
|
34
36
|
html = html.replace('</head>', `${remergeScript}${comment}${meta}</head>`);
|
|
35
37
|
// 注入统一的 CTA 适配器
|
|
@@ -49,8 +51,8 @@ export class RemergeAdapter extends PlatformAdapter {
|
|
|
49
51
|
`;
|
|
50
52
|
}
|
|
51
53
|
validateOptions() {
|
|
52
|
-
if (this.options.format && this.options.format !== '
|
|
53
|
-
console.warn('警告: Remerge
|
|
54
|
+
if (this.options.format && this.options.format !== 'zip') {
|
|
55
|
+
console.warn('警告: Remerge 交付 ZIP 格式');
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
}
|
|
@@ -197,10 +197,10 @@ export const CHANNEL_RULE_PROFILES = {
|
|
|
197
197
|
requiredPatternRule('adikteev', 'click-pixel-url.required', 'Adikteev requires AK_CLICK_PIXEL_URL exposure.', 'AK_CLICK_PIXEL_URL|AK CLICK PIXEL URL', 'Expose the Adikteev click pixel URL variable.'),
|
|
198
198
|
manualRule('adikteev', 'runtime.cdn-responsive.manual', 'CDN resource loading and responsive layout need runtime review.', 'Verify separated assets load from Adikteev CDN and remain responsive.'),
|
|
199
199
|
], { optionalStages: ['delivery-package', 'runtime-manual'], deliveryPackageFormat: 'fragment-assets' }),
|
|
200
|
-
remerge: profile('remerge', 'Remerge', '
|
|
200
|
+
remerge: profile('remerge', 'Remerge', 'zip', [
|
|
201
201
|
externalResourceRule('remerge'),
|
|
202
202
|
forbiddenPatternRule('remerge', 'redirect.auto.forbidden', 'Remerge creatives should not auto-redirect to the store.', '\\blocation\\.(href|assign|replace)\\b', 'Keep store jumps behind a user CTA.'),
|
|
203
|
-
]),
|
|
203
|
+
], { optionalStages: ['delivery-package'], deliveryPackageFormat: 'zip' }),
|
|
204
204
|
mintegral: profile('mintegral', 'Mintegral', 'mixed', [
|
|
205
205
|
sourcePresenceRule('mintegral', 'lifecycle.game-end.required', 'Mintegral requires a gameplay finish signal.', 'Call sdk.playcraftFinish() when gameplay reaches a win or end state.'),
|
|
206
206
|
requiredPatternRule('mintegral', 'lifecycle.game-end.required', 'Mintegral creatives should call window.gameEnd() after gameplay ends.', 'window\\.gameEnd\\s*\\(', 'Map the gameplay finish signal to window.gameEnd().'),
|
|
@@ -36,7 +36,7 @@ export const PLATFORM_CONFIGS = {
|
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
38
|
facebook: {
|
|
39
|
-
sizeLimit: 5 * 1024 * 1024, // 5MB(HTML/ZIP
|
|
39
|
+
sizeLimit: 5 * 1024 * 1024, // 5MB(HTML/ZIP 总包体;Phaser 产物为 ZIP)
|
|
40
40
|
outputFormat: 'html',
|
|
41
41
|
minifyCSS: true,
|
|
42
42
|
minifyJS: true,
|
|
@@ -388,7 +388,8 @@ export const PLATFORM_CONFIGS = {
|
|
|
388
388
|
},
|
|
389
389
|
remerge: {
|
|
390
390
|
sizeLimit: 5 * 1024 * 1024, // 5MB
|
|
391
|
-
|
|
391
|
+
// Remerge 交付 ZIP(与 devkit 产物一致)
|
|
392
|
+
outputFormat: 'zip',
|
|
392
393
|
minifyCSS: true,
|
|
393
394
|
minifyJS: true,
|
|
394
395
|
compressImages: false, // 禁用图片压缩,原图输出效果更好
|