@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 CHANGED
@@ -177,7 +177,7 @@ const adapter = createPlatformAdapter({
177
177
  });
178
178
 
179
179
  console.log(adapter.getName()); // "Facebook"
180
- console.log(adapter.getSizeLimit()); // 2097152 (2MB)
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
- * PlayCraft 不支持的渠道(playable-scripts 不认识的 PlayCraft 平台)
23
+ * devkit(playable-scripts)不支持的渠道——这些渠道会被从 `--channels` 中剔除。
24
+ *
25
+ * 目前为空:devkit 已覆盖 PlayCraft 全部渠道(含 remerge,产物为 ZIP)。
26
+ * 若将来新增 PlayCraft 有、devkit 无的渠道,在此登记即可恢复过滤行为。
24
27
  */
25
- const UNSUPPORTED_CHANNELS = new Set(['remerge']);
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 或构建请求 format=zip(如 Google / Facebook / TikTok 等选 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: HTML 5MB, ZIP 5MB(HTML 文件需 < 2MB 由校验处理)
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
- return 'html';
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: 单文件 HTML <5MB, CTA 无参数调用 -->`;
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 !== 'html') {
53
- console.warn('警告: Remerge 使用单文件 HTML 格式');
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', 'single-html', [
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
- outputFormat: 'html',
391
+ // Remerge 交付 ZIP(与 devkit 产物一致)
392
+ outputFormat: 'zip',
392
393
  minifyCSS: true,
393
394
  minifyJS: true,
394
395
  compressImages: false, // 禁用图片压缩,原图输出效果更好
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/build",
3
- "version": "0.0.57",
3
+ "version": "0.0.58",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",