@playcraft/devkit 1.0.27 → 1.0.28
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/cli/commands/build.js +73 -22
- package/core/batch-build.js +221 -51
- package/core/index.d.ts +8 -4
- package/core/options.js +23 -7
- package/core/plugins/ExitAPIInjectorPlugin.js +4 -5
- package/core/plugins/PlaycraftOrientationInjectorPlugin.js +33 -0
- package/core/utils/buildDefines.js +14 -12
- package/core/utils/buildTemplateString.js +0 -1
- package/core/utils/getPlaycraftOrientationSuffix.js +13 -0
- package/core/utils/injectSDKPlugins.js +3 -3
- package/core/utils/logOptions.js +10 -1
- package/core/utils/parseArgvOptions.js +3 -3
- package/core/webpack.build.js +38 -7
- package/core/webpack.dev.js +8 -2
- package/defines.d.ts +1 -1
- package/package.json +1 -1
package/cli/commands/build.js
CHANGED
|
@@ -10,6 +10,7 @@ const { runThemeValidation, restoreSchema } = require('../../core/utils/validate
|
|
|
10
10
|
const { runPreBuildChecks } = require('../../core/validators/pre-build-checker.js');
|
|
11
11
|
const { FontInjectionWebpackPlugin } = require('../../core/plugins/FontInjectionWebpackPlugin.js');
|
|
12
12
|
const { SocialMediaInjectorPlugin } = require('../../core/plugins/SocialMediaInjectorPlugin.js');
|
|
13
|
+
const { getPlaycraftOrientationSuffix } = require('../../core/utils/getPlaycraftOrientationSuffix.js');
|
|
13
14
|
const { options } = require('../../core/options.js');
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -157,30 +158,80 @@ function continueWithBuild() {
|
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
160
|
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.
|
|
161
|
+
// ============================================================
|
|
162
|
+
// 📌 横竖屏方向多版本构建
|
|
163
|
+
// ============================================================
|
|
164
|
+
// playcraftOrientationChannel 中指定的渠道,需要按 playcraftOrientation
|
|
165
|
+
// 列出的每个方向版本分别打一个包(渠道_L / 渠道_P / 渠道,即 both 无后缀);
|
|
166
|
+
// 不在列表中的渠道始终只按 both(自适应)打一个包。
|
|
167
|
+
const orientationChannels = options['playcraftOrientationChannel'] || [];
|
|
168
|
+
const orientationVersions =
|
|
169
|
+
Array.isArray(options['playcraftOrientation']) && options['playcraftOrientation'].length > 0
|
|
170
|
+
? options['playcraftOrientation']
|
|
171
|
+
: ['both'];
|
|
172
|
+
const buildOrientations = orientationChannels.includes(adNetwork) ? orientationVersions : ['both'];
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 构建单个方向版本
|
|
176
|
+
*/
|
|
177
|
+
async function buildOneOrientation(orientationValue) {
|
|
178
|
+
const suffix = getPlaycraftOrientationSuffix(orientationValue);
|
|
179
|
+
if (buildOrientations.length > 1) {
|
|
180
|
+
console.log(`\n📱 开始构建方向版本: ${orientationValue}${suffix ? ` (后缀: ${suffix})` : ''}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 注入字体插件(build 模式 base64 内联)
|
|
184
|
+
const fontPlugin = themeName
|
|
185
|
+
? new FontInjectionWebpackPlugin({ projectRoot, themeName, themeConfig })
|
|
186
|
+
: null;
|
|
187
|
+
|
|
188
|
+
// social_media 渠道:注入 OG / Twitter meta 标签,让 Facebook / LinkedIn
|
|
189
|
+
// 等抓取预览时拿到正确的标题/描述/图片。无 shareMeta 时空跑,不影响构建
|
|
190
|
+
const socialMediaPlugin = (themeName && adNetwork === 'social_media')
|
|
191
|
+
? new SocialMediaInjectorPlugin({ projectRoot, themeName, themeConfig })
|
|
192
|
+
: null;
|
|
193
|
+
|
|
194
|
+
await runBuild(
|
|
195
|
+
undefined,
|
|
196
|
+
{
|
|
197
|
+
playcraftOrientationValue: orientationValue,
|
|
198
|
+
filename: suffix ? `${options.filename}${suffix}` : options.filename
|
|
199
|
+
},
|
|
200
|
+
undefined,
|
|
201
|
+
{
|
|
202
|
+
plugins: [
|
|
203
|
+
...(fontPlugin ? [fontPlugin] : []),
|
|
204
|
+
...(socialMediaPlugin ? [socialMediaPlugin] : []),
|
|
205
|
+
],
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
(async () => {
|
|
211
|
+
try {
|
|
212
|
+
// 注意:方向版本之间故意串行执行(不用 Promise.all 并发)。
|
|
213
|
+
// 原因:obfuscateLevel=4 等重度混淆/压缩下单次 webpack 编译本身就
|
|
214
|
+
// 会占用数 GB 内存;而该 build.js 进程本身通常已是 batch-build.js
|
|
215
|
+
// 渠道级并发中的一个子进程(多个渠道同时在跑)。若在这里再叠加同
|
|
216
|
+
// 进程内的多方向并发(多个 webpack compiler 共享同一份 V8 堆),
|
|
217
|
+
// 内存需求会成倍叠加,实测会导致 "JavaScript heap out of memory"
|
|
218
|
+
// 崩溃。因此并发只在渠道层(各自独立进程、独立堆)进行,方向版本
|
|
219
|
+
// 层始终串行,以保证稳定性。
|
|
220
|
+
for (const orientationValue of buildOrientations) {
|
|
221
|
+
await buildOneOrientation(orientationValue);
|
|
222
|
+
}
|
|
223
|
+
|
|
179
224
|
if (schemaPathToRestore) restoreSchema(schemaPathToRestore);
|
|
180
|
-
|
|
181
|
-
|
|
225
|
+
|
|
226
|
+
// 主动退出进程,避免 TerserPlugin/webpack-obfuscator 等可能残留的
|
|
227
|
+
// worker 进程句柄(jest-worker 子进程 IPC 通道等)迟迟不释放,
|
|
228
|
+
// 导致本次 build 子进程(乃至上层 batch-build 的 spawn 等待)
|
|
229
|
+
// 迟迟无法结束。此时构建已全部完成,不再有需要等待的异步操作。
|
|
230
|
+
process.exit(0);
|
|
231
|
+
} catch (err) {
|
|
182
232
|
if (schemaPathToRestore) restoreSchema(schemaPathToRestore);
|
|
183
233
|
console.error(err);
|
|
184
234
|
process.exit(1);
|
|
185
|
-
}
|
|
235
|
+
}
|
|
236
|
+
})();
|
|
186
237
|
}
|
package/core/batch-build.js
CHANGED
|
@@ -12,16 +12,83 @@
|
|
|
12
12
|
* 4. 支持灵活的配置系统
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
const { execSync,
|
|
15
|
+
const { execSync, spawn } = require('child_process');
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const path = require('path');
|
|
18
18
|
const os = require('os');
|
|
19
|
-
const { promisify } = require('util');
|
|
20
19
|
const { fetchTemporaryCredentials, createCOSClient, uploadDirectoryToCOS, uploadFileToCOS, purgeCDNCache } = require('./cos-uploader');
|
|
21
20
|
const { runThemeValidation, restoreSchema } = require('./utils/validateThemeData');
|
|
22
21
|
const { runPreBuildChecks } = require('./validators/pre-build-checker.js');
|
|
22
|
+
const { getPlaycraftOrientationSuffix } = require('./utils/getPlaycraftOrientationSuffix.js');
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* 执行构建命令,实时把 stdout/stderr 落盘到日志文件,而不是像 exec()
|
|
26
|
+
* 那样把全部输出攒在内存里的一个 buffer 中。
|
|
27
|
+
*
|
|
28
|
+
* 背景:exec() 的 maxBuffer 是硬性内存上限(原来设为 10MB)。当某个渠道
|
|
29
|
+
* 需要连续构建多个方向版本(playcraftOrientationChannel)时,日志量会
|
|
30
|
+
* 成倍增加,很容易超过该上限触发 "stderr maxBuffer length exceeded" 而
|
|
31
|
+
* 直接杀掉子进程。改用 spawn 边收边写文件后,日志大小不再受内存限制;
|
|
32
|
+
* 内存中只保留末尾一小段用于失败时快速定位问题,完整日志可在日志文件中查看。
|
|
33
|
+
*
|
|
34
|
+
* @param {string} command - 要执行的命令(通过 shell 解析,支持带引号的参数)
|
|
35
|
+
* @param {object} options - spawn 选项(cwd/env 等)
|
|
36
|
+
* @param {string} logPath - 日志文件落盘路径
|
|
37
|
+
* @returns {Promise<{logPath: string}>}
|
|
38
|
+
*/
|
|
39
|
+
function execWithLoggedOutput(command, options, logPath) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
42
|
+
const logStream = fs.createWriteStream(logPath, { flags: 'w' });
|
|
43
|
+
|
|
44
|
+
const TAIL_LIMIT = 8000; // 失败时用于在错误信息中展示的日志末尾字符数
|
|
45
|
+
let tail = '';
|
|
46
|
+
const appendTail = (chunk) => {
|
|
47
|
+
tail += chunk;
|
|
48
|
+
if (tail.length > TAIL_LIMIT) {
|
|
49
|
+
tail = tail.slice(tail.length - TAIL_LIMIT);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let child;
|
|
54
|
+
try {
|
|
55
|
+
child = spawn(command, { ...options, shell: true });
|
|
56
|
+
} catch (err) {
|
|
57
|
+
logStream.end();
|
|
58
|
+
reject(err);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
child.stdout && child.stdout.on('data', (chunk) => {
|
|
63
|
+
logStream.write(chunk);
|
|
64
|
+
appendTail(chunk.toString());
|
|
65
|
+
});
|
|
66
|
+
child.stderr && child.stderr.on('data', (chunk) => {
|
|
67
|
+
logStream.write(chunk);
|
|
68
|
+
appendTail(chunk.toString());
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
child.on('error', (err) => {
|
|
72
|
+
logStream.end();
|
|
73
|
+
reject(err);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
child.on('close', (code) => {
|
|
77
|
+
logStream.end(() => {
|
|
78
|
+
if (code === 0) {
|
|
79
|
+
resolve({ logPath });
|
|
80
|
+
} else {
|
|
81
|
+
const err = new Error(
|
|
82
|
+
`子进程退出码 ${code}\n---- 日志末尾 ----\n${tail}\n---- 完整日志见: ${logPath} ----`
|
|
83
|
+
);
|
|
84
|
+
err.logPath = logPath;
|
|
85
|
+
err.exitCode = code;
|
|
86
|
+
reject(err);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
25
92
|
|
|
26
93
|
/**
|
|
27
94
|
* 更新 preview.json 文件
|
|
@@ -359,14 +426,64 @@ function copyDirRecursive(src, dest) {
|
|
|
359
426
|
}
|
|
360
427
|
}
|
|
361
428
|
|
|
429
|
+
/**
|
|
430
|
+
* 根据 playcraftOrientationChannel / playcraftOrientation 配置,把渠道列表
|
|
431
|
+
* 展开为「渠道构建任务」列表:需要多方向版本的渠道会被拆分成多个独立任务
|
|
432
|
+
* (对应 渠道_L / 渠道_P / 渠道 三个方向),每个任务各自对应一次独立的
|
|
433
|
+
* 子进程构建。
|
|
434
|
+
*
|
|
435
|
+
* 背景:此前多方向版本是在单个子进程(cli/commands/build.js)内部循环
|
|
436
|
+
* 串行构建的,obfuscateLevel=4 等重度混淆下单次编译本身就占用较大内存/
|
|
437
|
+
* 日志量,3 个方向串行叠加很容易撑爆内存或撑爆日志 buffer。改为在这里
|
|
438
|
+
* 提前拆分任务后,多方向构建就和普通的多渠道构建一样,交给已有的
|
|
439
|
+
* `buildChannelsInParallel` 并发调度(各任务独立进程、独立堆、独立日志
|
|
440
|
+
* 文件),从而把「单进程内串行」变成「渠道层面的多进程并发」。
|
|
441
|
+
*
|
|
442
|
+
* @param {string[]} channels - 原始渠道列表(已包含 'preview')
|
|
443
|
+
* @param {object} config - 批量构建配置
|
|
444
|
+
* @returns {{channel: string, orientation: ('both'|'landscape'|'portrait'|null)}[]}
|
|
445
|
+
*/
|
|
446
|
+
function buildChannelTasks(channels, config) {
|
|
447
|
+
const orientationChannels = config.playcraftOrientationChannel || [];
|
|
448
|
+
const orientationVersions =
|
|
449
|
+
Array.isArray(config.playcraftOrientation) && config.playcraftOrientation.length > 0
|
|
450
|
+
? config.playcraftOrientation
|
|
451
|
+
: ['both'];
|
|
452
|
+
|
|
453
|
+
const tasks = [];
|
|
454
|
+
for (const channel of channels) {
|
|
455
|
+
// preview 渠道用于收集预览文件,不参与多方向拆分
|
|
456
|
+
if (channel !== 'preview' && orientationChannels.includes(channel)) {
|
|
457
|
+
for (const orientation of orientationVersions) {
|
|
458
|
+
tasks.push({ channel, orientation });
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
tasks.push({ channel, orientation: null });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return tasks;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* 生成任务展示标签,用于日志中区分同一渠道下的不同方向任务,如 google(landscape) → google_L
|
|
469
|
+
*/
|
|
470
|
+
function getTaskLabel(channel, orientation) {
|
|
471
|
+
if (!orientation) return channel;
|
|
472
|
+
const suffix = getPlaycraftOrientationSuffix(orientation);
|
|
473
|
+
return suffix ? `${channel}${suffix}` : channel;
|
|
474
|
+
}
|
|
475
|
+
|
|
362
476
|
/**
|
|
363
477
|
* 构建单个渠道(使用独立临时目录)
|
|
478
|
+
* @param {('both'|'landscape'|'portrait'|null)} [orientation] - 指定要强制构建的单一方向版本;
|
|
479
|
+
* 为 null/undefined 时不干预,由渠道自身默认逻辑决定(通常即 'both')
|
|
364
480
|
*/
|
|
365
|
-
async function buildChannel(config, themeName, channel, outputDir, projectRoot, isChannelFold2Zip) {
|
|
481
|
+
async function buildChannel(config, themeName, channel, outputDir, projectRoot, isChannelFold2Zip, orientation) {
|
|
366
482
|
const taskId = generateTaskId();
|
|
367
483
|
const tempDir = createTempDir(config, projectRoot, taskId);
|
|
484
|
+
const taskLabel = getTaskLabel(channel, orientation);
|
|
368
485
|
|
|
369
|
-
log(`开始构建: 主题=${themeName}, 渠道=${
|
|
486
|
+
log(`开始构建: 主题=${themeName}, 渠道=${taskLabel}, 任务ID=${taskId.substr(0, 8)}...`);
|
|
370
487
|
|
|
371
488
|
try {
|
|
372
489
|
// 生成文件名
|
|
@@ -380,7 +497,7 @@ async function buildChannel(config, themeName, channel, outputDir, projectRoot,
|
|
|
380
497
|
// 注意:
|
|
381
498
|
// 1. 命令行参数使用 --out-dir (连字符格式)
|
|
382
499
|
// 2. 通过环境变量 PLAYABLE_FILENAME 传递文件名,避免并发时修改 build.json 导致竞态条件
|
|
383
|
-
// 3. 使用
|
|
500
|
+
// 3. 使用 spawn + 落盘日志实现真正的并发构建(避免 exec 的内存 buffer 上限)
|
|
384
501
|
// 4. 通过命令行参数传递商店链接(如果配置中有定义)
|
|
385
502
|
|
|
386
503
|
// 构建命令行参数
|
|
@@ -398,23 +515,51 @@ async function buildChannel(config, themeName, channel, outputDir, projectRoot,
|
|
|
398
515
|
if (isChannelFold2Zip) {
|
|
399
516
|
buildArgs.push('--is-channel-fold2zip');
|
|
400
517
|
}
|
|
518
|
+
|
|
519
|
+
// 指定单一方向版本:显式传入仅含该方向的 --playcraft-orientation,
|
|
520
|
+
// 并把当前渠道加入 --playcraft-orientation-channel,使子进程内
|
|
521
|
+
// (cli/commands/build.js)的方向循环只会执行这一个方向,从而把
|
|
522
|
+
// 「单进程内串行构建多个方向」变成「渠道层面的多进程并发构建」。
|
|
523
|
+
if (orientation) {
|
|
524
|
+
buildArgs.push(`--playcraft-orientation ${orientation}`);
|
|
525
|
+
buildArgs.push(`--playcraft-orientation-channel ${channel}`);
|
|
526
|
+
}
|
|
401
527
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
528
|
+
const buildLogPath = path.join(
|
|
529
|
+
projectRoot,
|
|
530
|
+
'.playable-build-logs',
|
|
531
|
+
`${themeName}_${taskLabel}_${taskId.substr(0, 8)}.log`
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
await execWithLoggedOutput(`npm run build -- ${buildArgs.join(' ')}`, {
|
|
536
|
+
cwd: projectRoot,
|
|
537
|
+
env: {
|
|
538
|
+
...process.env,
|
|
539
|
+
// 兜底:即使 webpack compiler 已正确 close(),重度混淆(obfuscateLevel=4)
|
|
540
|
+
// 下多方向串行构建仍可能瞬时占用较大堆内存,此处适当调高上限做防御
|
|
541
|
+
// (不覆盖用户已自行设置的 NODE_OPTIONS)
|
|
542
|
+
NODE_OPTIONS: process.env.NODE_OPTIONS || '--max-old-space-size=8192',
|
|
543
|
+
PLAYABLE_BUILD_TASK_ID: taskId,
|
|
544
|
+
PLAYABLE_FILENAME: fileName, // 通过环境变量传递文件名
|
|
545
|
+
PLAYABLE_BATCH_MODE: '1' // 标识批量构建模式,子进程跳过 runThemeValidation(主进程已处理)
|
|
546
|
+
}
|
|
547
|
+
}, buildLogPath);
|
|
548
|
+
|
|
549
|
+
// 构建成功后日志已无需保留,删除避免日志文件堆积
|
|
550
|
+
fs.rmSync(buildLogPath, { force: true });
|
|
551
|
+
} catch (buildError) {
|
|
552
|
+
// 失败时保留日志文件,方便排查(不在这里删除)
|
|
553
|
+
throw buildError;
|
|
554
|
+
}
|
|
413
555
|
|
|
414
556
|
// 复制 tempDir 所有文件到目标目录
|
|
415
|
-
// 第二级目录名使用 "主题名_渠道名"
|
|
557
|
+
// 第二级目录名使用 "主题名_渠道名[_L|_P]",避免不同主题下同名渠道目录/文件混淆;
|
|
558
|
+
// 若该渠道被 playcraftOrientationChannel 拆分为多方向任务,还需带上方向后缀,
|
|
559
|
+
// 否则 landscape/portrait/both 三个并发任务会把产物混进同一个文件夹
|
|
416
560
|
function copyTempFiles(outputDir, themeName, channel) {
|
|
417
|
-
const
|
|
561
|
+
const orientationSuffix = orientation ? getPlaycraftOrientationSuffix(orientation) : '';
|
|
562
|
+
const channelDirName = `${themeName}_${channel}${orientationSuffix}`;
|
|
418
563
|
const targetDir = path.join(outputDir, themeName, channelDirName);
|
|
419
564
|
if (!fs.existsSync(targetDir)) {
|
|
420
565
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
@@ -432,27 +577,31 @@ async function buildChannel(config, themeName, channel, outputDir, projectRoot,
|
|
|
432
577
|
}
|
|
433
578
|
});
|
|
434
579
|
|
|
435
|
-
log(`✓ 构建成功: ${themeName}/${
|
|
580
|
+
log(`✓ 构建成功: ${themeName}/${taskLabel} → ${themeName}/${channelDirName}`, 'success');
|
|
436
581
|
}
|
|
437
582
|
|
|
438
583
|
// 移动构建产物到目标目录
|
|
439
584
|
if (fs.existsSync(tempDir)) {
|
|
440
585
|
if (isChannelFold2Zip && channel !== 'preview') {
|
|
441
|
-
// isChannelFold2Zip
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const
|
|
586
|
+
// isChannelFold2Zip 模式:移动 渠道名[.zip|_L.zip|_P.zip] 到 outputDir/themeName/主题名_渠道名[_L|_P].zip
|
|
587
|
+
// 注:若该渠道被配置为多方向版本构建(playcraftOrientationChannel),tempDir 内
|
|
588
|
+
// 会同时存在 channel.zip / channel_L.zip / channel_P.zip 中的一个或多个,需要全部匹配并逐个复制
|
|
589
|
+
const zipRegex = new RegExp(`^${channel}(_L|_P)?\\.zip$`);
|
|
445
590
|
const themeDir = path.join(outputDir, themeName);
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
if (
|
|
591
|
+
const matchedZipFiles = fs.readdirSync(tempDir).filter((file) => zipRegex.test(file));
|
|
592
|
+
|
|
593
|
+
if (matchedZipFiles.length > 0) {
|
|
449
594
|
if (!fs.existsSync(themeDir)) {
|
|
450
595
|
fs.mkdirSync(themeDir, { recursive: true });
|
|
451
596
|
}
|
|
452
|
-
|
|
453
|
-
|
|
597
|
+
matchedZipFiles.forEach((zipFileName) => {
|
|
598
|
+
const suffix = zipFileName.match(zipRegex)[1] || '';
|
|
599
|
+
const destZipFileName = `${themeName}_${channel}${suffix}.zip`;
|
|
600
|
+
fs.copyFileSync(path.join(tempDir, zipFileName), path.join(themeDir, destZipFileName));
|
|
601
|
+
log(`✓ 构建成功: ${themeName}/${channel}${suffix} → ${themeName}/${destZipFileName}`, 'success');
|
|
602
|
+
});
|
|
454
603
|
} else {
|
|
455
|
-
log(`⚠️ 未找到 zip 文件: ${
|
|
604
|
+
log(`⚠️ 未找到 zip 文件: ${channel}*.zip,回退到普通文件移动`, 'warn');
|
|
456
605
|
// 回退:移动所有文件到 outputDir/themeName/主题名_渠道名/
|
|
457
606
|
copyTempFiles(outputDir, themeName, channel);
|
|
458
607
|
}
|
|
@@ -467,9 +616,9 @@ async function buildChannel(config, themeName, channel, outputDir, projectRoot,
|
|
|
467
616
|
cleanTempDir(tempDir);
|
|
468
617
|
}
|
|
469
618
|
|
|
470
|
-
return { success: true, theme: themeName, channel };
|
|
619
|
+
return { success: true, theme: themeName, channel, orientation };
|
|
471
620
|
} catch (error) {
|
|
472
|
-
log(`构建失败: ${themeName}/${
|
|
621
|
+
log(`构建失败: ${themeName}/${taskLabel} - ${error.message}`, 'error');
|
|
473
622
|
|
|
474
623
|
// 清理临时目录
|
|
475
624
|
if (config.build.cleanTemp) {
|
|
@@ -480,22 +629,22 @@ async function buildChannel(config, themeName, channel, outputDir, projectRoot,
|
|
|
480
629
|
throw error;
|
|
481
630
|
}
|
|
482
631
|
|
|
483
|
-
return { success: false, theme: themeName, channel, error: error.message };
|
|
632
|
+
return { success: false, theme: themeName, channel, orientation, error: error.message };
|
|
484
633
|
}
|
|
485
634
|
}
|
|
486
635
|
|
|
487
636
|
/**
|
|
488
|
-
*
|
|
637
|
+
* 并发构建多个渠道任务(一个任务 = 一个渠道的一个方向版本,preview 渠道无方向拆分)
|
|
489
638
|
*/
|
|
490
|
-
async function buildChannelsInParallel(config, themeName,
|
|
639
|
+
async function buildChannelsInParallel(config, themeName, tasks, outputDir, projectRoot, isChannelFold2Zip) {
|
|
491
640
|
const maxParallel = config.build.parallel || 3;
|
|
492
641
|
const results = [];
|
|
493
642
|
|
|
494
643
|
// 分批并发构建
|
|
495
|
-
for (let i = 0; i <
|
|
496
|
-
const batch =
|
|
497
|
-
const batchPromises = batch.map(
|
|
498
|
-
buildChannel(config, themeName, channel, outputDir, projectRoot, isChannelFold2Zip)
|
|
644
|
+
for (let i = 0; i < tasks.length; i += maxParallel) {
|
|
645
|
+
const batch = tasks.slice(i, i + maxParallel);
|
|
646
|
+
const batchPromises = batch.map(task =>
|
|
647
|
+
buildChannel(config, themeName, task.channel, outputDir, projectRoot, isChannelFold2Zip, task.orientation)
|
|
499
648
|
);
|
|
500
649
|
|
|
501
650
|
const batchResults = await Promise.allSettled(batchPromises);
|
|
@@ -507,7 +656,8 @@ async function buildChannelsInParallel(config, themeName, channels, outputDir, p
|
|
|
507
656
|
results.push({
|
|
508
657
|
success: false,
|
|
509
658
|
theme: themeName,
|
|
510
|
-
channel: batch[index],
|
|
659
|
+
channel: batch[index].channel,
|
|
660
|
+
orientation: batch[index].orientation,
|
|
511
661
|
error: result.reason?.message || '未知错误'
|
|
512
662
|
});
|
|
513
663
|
}
|
|
@@ -818,9 +968,13 @@ function mergeConfig(defaultConfig, userConfig) {
|
|
|
818
968
|
|
|
819
969
|
/**
|
|
820
970
|
* 打印配置摘要
|
|
971
|
+
* @param {object} config
|
|
972
|
+
* @param {string[]} themes
|
|
973
|
+
* @param {string[]} channels - 原始渠道列表(未展开方向)
|
|
974
|
+
* @param {{channel:string, orientation:(string|null)}[]} tasks - 展开方向后的实际构建任务列表
|
|
821
975
|
*/
|
|
822
|
-
function printSummary(config, themes, channels) {
|
|
823
|
-
const totalBuilds = themes.length *
|
|
976
|
+
function printSummary(config, themes, channels, tasks) {
|
|
977
|
+
const totalBuilds = themes.length * tasks.length;
|
|
824
978
|
const cpuCount = os.cpus().length;
|
|
825
979
|
const cpuModel = os.cpus()[0]?.model || 'Unknown';
|
|
826
980
|
|
|
@@ -829,6 +983,9 @@ function printSummary(config, themes, channels) {
|
|
|
829
983
|
log(`项目: ${config.title}`);
|
|
830
984
|
log(`主题数: ${themes.length}`);
|
|
831
985
|
log(`渠道数: ${channels.length}`);
|
|
986
|
+
if (tasks.length !== channels.length) {
|
|
987
|
+
log(`构建任务数(展开多方向后): ${tasks.length}`);
|
|
988
|
+
}
|
|
832
989
|
log(`总构建数: ${totalBuilds}`);
|
|
833
990
|
log(`CPU: ${cpuCount} 核心 (${cpuModel.substring(0, 40)}${cpuModel.length > 40 ? '...' : ''})`);
|
|
834
991
|
log(`并发数: ${config.build.parallel} (自动优化)`);
|
|
@@ -838,6 +995,10 @@ function printSummary(config, themes, channels) {
|
|
|
838
995
|
log(`主题列表: ${themes.join(', ')}`, 'debug');
|
|
839
996
|
}
|
|
840
997
|
log(`渠道列表: ${channels.join(', ')}`, 'debug');
|
|
998
|
+
if (tasks.length !== channels.length) {
|
|
999
|
+
const taskLabels = tasks.map(t => getTaskLabel(t.channel, t.orientation));
|
|
1000
|
+
log(`已按 playcraftOrientationChannel 展开为独立并发任务: ${taskLabels.join(', ')}`, 'info');
|
|
1001
|
+
}
|
|
841
1002
|
log('');
|
|
842
1003
|
}
|
|
843
1004
|
|
|
@@ -888,6 +1049,11 @@ async function buildBatch() {
|
|
|
888
1049
|
log('已添加 preview 渠道', 'info');
|
|
889
1050
|
}
|
|
890
1051
|
|
|
1052
|
+
// 根据 playcraftOrientationChannel / playcraftOrientation 配置,把需要
|
|
1053
|
+
// 多方向版本的渠道拆分为独立的构建任务(渠道_L / 渠道_P / 渠道),
|
|
1054
|
+
// 后续统一交给渠道级并发调度,每个方向各自独立进程构建。
|
|
1055
|
+
const tasks = buildChannelTasks(channels, config);
|
|
1056
|
+
|
|
891
1057
|
// ============================================================
|
|
892
1058
|
// 📌 编译前检查(埋点验证、主题名称检查等)- 仅在主进程执行一次
|
|
893
1059
|
// ============================================================
|
|
@@ -915,7 +1081,7 @@ async function buildBatch() {
|
|
|
915
1081
|
}
|
|
916
1082
|
|
|
917
1083
|
// 打印摘要
|
|
918
|
-
printSummary(config, themes, channels);
|
|
1084
|
+
printSummary(config, themes, channels, tasks);
|
|
919
1085
|
|
|
920
1086
|
// 备份入口文件(如果启用主题切换)
|
|
921
1087
|
if (config.themes.enabled) {
|
|
@@ -955,20 +1121,20 @@ async function buildBatch() {
|
|
|
955
1121
|
if (!config.build.continueOnError) {
|
|
956
1122
|
throw new Error(`Theme data validation failed for "${theme}"`);
|
|
957
1123
|
}
|
|
958
|
-
// Record failures for all
|
|
959
|
-
|
|
960
|
-
allResults.push({ success: false, theme, channel, error: 'Theme data validation failed' });
|
|
1124
|
+
// Record failures for all tasks of this theme and skip building
|
|
1125
|
+
tasks.forEach(({ channel, orientation }) => {
|
|
1126
|
+
allResults.push({ success: false, theme, channel, orientation, error: 'Theme data validation failed' });
|
|
961
1127
|
});
|
|
962
1128
|
continue;
|
|
963
1129
|
}
|
|
964
1130
|
|
|
965
|
-
//
|
|
1131
|
+
// 并发构建该主题的所有渠道任务(多方向渠道已展开为独立任务)
|
|
966
1132
|
let themeResults;
|
|
967
1133
|
try {
|
|
968
1134
|
themeResults = await buildChannelsInParallel(
|
|
969
1135
|
config,
|
|
970
1136
|
theme,
|
|
971
|
-
|
|
1137
|
+
tasks,
|
|
972
1138
|
outputDir,
|
|
973
1139
|
projectRoot,
|
|
974
1140
|
isChannelFold2Zip
|
|
@@ -1039,7 +1205,7 @@ async function buildBatch() {
|
|
|
1039
1205
|
|
|
1040
1206
|
// 打印进度
|
|
1041
1207
|
const completedBuilds = allResults.length;
|
|
1042
|
-
const totalBuilds = themes.length *
|
|
1208
|
+
const totalBuilds = themes.length * tasks.length;
|
|
1043
1209
|
const progress = Math.round(completedBuilds / totalBuilds * 100);
|
|
1044
1210
|
log(`进度: ${completedBuilds}/${totalBuilds} (${progress}%)`, 'success');
|
|
1045
1211
|
}
|
|
@@ -1090,8 +1256,8 @@ async function buildBatch() {
|
|
|
1090
1256
|
|
|
1091
1257
|
if (failedBuilds.length > 0) {
|
|
1092
1258
|
log(`✗ 失败构建: ${failedBuilds.length}`, 'warn');
|
|
1093
|
-
failedBuilds.forEach(({ theme, channel, error }) => {
|
|
1094
|
-
log(` - ${theme}/${channel}: ${error}`, 'error');
|
|
1259
|
+
failedBuilds.forEach(({ theme, channel, orientation, error }) => {
|
|
1260
|
+
log(` - ${theme}/${getTaskLabel(channel, orientation)}: ${error}`, 'error');
|
|
1095
1261
|
});
|
|
1096
1262
|
}
|
|
1097
1263
|
|
|
@@ -1126,6 +1292,10 @@ async function buildBatch() {
|
|
|
1126
1292
|
|
|
1127
1293
|
log('========================================');
|
|
1128
1294
|
|
|
1295
|
+
// 主动退出进程:COS SDK 的 https keep-alive 连接、子进程遗留的 IPC 句柄
|
|
1296
|
+
// 等都可能让事件循环迟迟无法自然清空,导致命令执行完毕后终端长时间不返回。
|
|
1297
|
+
// 此处所有任务已全部完成,无需再等待任何异步操作,直接退出即可。
|
|
1298
|
+
process.exit(0);
|
|
1129
1299
|
} catch (error) {
|
|
1130
1300
|
log(`严重错误: ${error.message}`, 'error');
|
|
1131
1301
|
console.error(error);
|
package/core/index.d.ts
CHANGED
|
@@ -46,8 +46,8 @@ export interface DefinesOptions {
|
|
|
46
46
|
BUILD_HASH: string;
|
|
47
47
|
/** Current language code */
|
|
48
48
|
LANGUAGE: string;
|
|
49
|
-
/** Current orientation identifier */
|
|
50
|
-
|
|
49
|
+
/** Current playcraft orientation identifier (window.playcraftOrientation / PLAYCRAFT_ORIENTATION define) */
|
|
50
|
+
PLAYCRAFT_ORIENTATION: 'both' | 'landscape' | 'portrait';
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/** CLI configuration options */
|
|
@@ -110,8 +110,12 @@ export interface CLIOptions {
|
|
|
110
110
|
version: string;
|
|
111
111
|
/** Language code for localization */
|
|
112
112
|
language: 'auto' | 'en' | 'es' | 'zh' | 'hi' | 'ar' | 'fr' | 'de' | 'ja' | 'pt';
|
|
113
|
-
/**
|
|
114
|
-
|
|
113
|
+
/** 当前要打包的横竖屏方向版本列表(builds.config.js 专属),默认 ['both'] */
|
|
114
|
+
playcraftOrientation: ('both' | 'landscape' | 'portrait')[];
|
|
115
|
+
/** 哪些渠道需要按 playcraftOrientation 分别打出多个方向版本的包(builds.config.js 专属),默认 [] */
|
|
116
|
+
playcraftOrientationChannel: string[];
|
|
117
|
+
/** 当前单次构建实际使用的横竖屏方向版本值(内部循环时按需注入,非 CLI 参数),默认 'both' */
|
|
118
|
+
playcraftOrientationValue?: 'both' | 'landscape' | 'portrait';
|
|
115
119
|
/** Google Play Store URL for the app */
|
|
116
120
|
googlePlayUrl: string;
|
|
117
121
|
/** App Store URL for the app */
|
package/core/options.js
CHANGED
|
@@ -2,7 +2,7 @@ const {
|
|
|
2
2
|
parseArgvOptions,
|
|
3
3
|
allowedAdProtocols,
|
|
4
4
|
allowedAdNetworks,
|
|
5
|
-
|
|
5
|
+
allowedPlaycraftOrientations,
|
|
6
6
|
allowedLanguages
|
|
7
7
|
} = require('./utils/parseArgvOptions');
|
|
8
8
|
const path = require('path');
|
|
@@ -137,15 +137,31 @@ const possibleOptions = [
|
|
|
137
137
|
}
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
|
-
name: 'orientation',
|
|
141
|
-
|
|
140
|
+
name: 'playcraft-orientation',
|
|
141
|
+
alias: 'playcraftOrientation',
|
|
142
|
+
defaultValue: ['both'],
|
|
142
143
|
hasValue: true,
|
|
143
|
-
description:
|
|
144
|
+
description:
|
|
145
|
+
'当前要打包的横竖屏方向版本列表,可指定多个(逗号分隔),取值:landscape/portrait/both。用于 PLAYCRAFT_ORIENTATION define 及 window.playcraftOrientation 运行时变量',
|
|
144
146
|
parser: function (rawValue) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
+
const values = rawValue.split(',').map((v) => v.trim()).filter(Boolean);
|
|
148
|
+
for (const value of values) {
|
|
149
|
+
if (!allowedPlaycraftOrientations.includes(value)) {
|
|
150
|
+
throw new Error(`--playcraft-orientation should have one of the value: ${allowedPlaycraftOrientations.join(', ')}`);
|
|
151
|
+
}
|
|
147
152
|
}
|
|
148
|
-
return
|
|
153
|
+
return values;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'playcraft-orientation-channel',
|
|
158
|
+
alias: 'playcraftOrientationChannel',
|
|
159
|
+
defaultValue: [],
|
|
160
|
+
hasValue: true,
|
|
161
|
+
description:
|
|
162
|
+
'哪些渠道需要按 playcraftOrientation 分别打出多个方向版本的包(逗号分隔的渠道名列表)。不在此列表中的渠道始终按 both(自适应)单一版本打包',
|
|
163
|
+
parser: function (rawValue) {
|
|
164
|
+
return rawValue.split(',').map((v) => v.trim()).filter(Boolean);
|
|
149
165
|
}
|
|
150
166
|
},
|
|
151
167
|
{
|
|
@@ -10,10 +10,10 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
|
10
10
|
*/
|
|
11
11
|
class ExitAPIInjectorPlugin {
|
|
12
12
|
/**
|
|
13
|
-
* @param {
|
|
13
|
+
* @param {'both'|'landscape'|'portrait'} orientation - 当前构建的横竖屏方向版本,默认 'both'
|
|
14
14
|
*/
|
|
15
15
|
constructor(orientation) {
|
|
16
|
-
this.orientation = orientation;
|
|
16
|
+
this.orientation = orientation || 'both';
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
apply(compiler) {
|
|
@@ -32,8 +32,7 @@ class ExitAPIInjectorPlugin {
|
|
|
32
32
|
let width = 320;
|
|
33
33
|
let height = 480;
|
|
34
34
|
|
|
35
|
-
if (this.orientation === '
|
|
36
|
-
else if (this.orientation === 'landscape') {
|
|
35
|
+
if (this.orientation === 'landscape') {
|
|
37
36
|
width = 480;
|
|
38
37
|
height = 320;
|
|
39
38
|
}
|
|
@@ -51,7 +50,7 @@ class ExitAPIInjectorPlugin {
|
|
|
51
50
|
meta: { plugin: 'html-webpack-plugin' },
|
|
52
51
|
attributes: {
|
|
53
52
|
name: 'ad.orientation',
|
|
54
|
-
content: this.orientation === 'both'
|
|
53
|
+
content: this.orientation === 'both' ? 'portrait,landscape' : this.orientation
|
|
55
54
|
}
|
|
56
55
|
});
|
|
57
56
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 在 HTML <head> 最前面注入 window.playcraftOrientation 全局变量,
|
|
5
|
+
* 标识当前构建产物对应的横竖屏方向版本,供游戏/主题代码在运行时读取判断。
|
|
6
|
+
* @implements {import('webpack').WebpackPluginInstance}
|
|
7
|
+
*/
|
|
8
|
+
class PlaycraftOrientationInjectorPlugin {
|
|
9
|
+
/**
|
|
10
|
+
* @param {'both'|'landscape'|'portrait'} orientation - 当前构建的方向版本,默认 'both'
|
|
11
|
+
*/
|
|
12
|
+
constructor(orientation) {
|
|
13
|
+
this.orientation = orientation || 'both';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
apply(compiler) {
|
|
17
|
+
compiler.hooks.compilation.tap('PlaycraftOrientationInjectorPlugin', (compilation) => {
|
|
18
|
+
HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync('PlaycraftOrientationInjectorPlugin', (data, callback) => {
|
|
19
|
+
data.headTags.splice(0, 0, {
|
|
20
|
+
tagName: 'script',
|
|
21
|
+
voidTag: false,
|
|
22
|
+
meta: { plugin: 'html-inline-script-webpack-plugin' },
|
|
23
|
+
attributes: { type: 'text/javascript' },
|
|
24
|
+
innerHTML: `window.playcraftOrientation=${JSON.stringify(this.orientation)};`
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
callback(null, data);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
exports.PlaycraftOrientationInjectorPlugin = PlaycraftOrientationInjectorPlugin;
|
|
@@ -2,24 +2,26 @@ const { options } = require('../options');
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Build's default defines list for dev & prod builds
|
|
5
|
+
* @param {import('../index').CLIOptions} [customOptions] - 合并后的当前构建选项(用于读取会随单次构建变化的字段,如 playcraftOrientationValue);未提供时回退到全局 options
|
|
5
6
|
*/
|
|
6
|
-
exports.buildDefines = function buildDefines() {
|
|
7
|
+
exports.buildDefines = function buildDefines(customOptions) {
|
|
8
|
+
const opts = customOptions || options;
|
|
7
9
|
const defines = {
|
|
8
|
-
__DEV__: JSON.stringify(
|
|
9
|
-
GOOGLE_PLAY_URL: JSON.stringify(
|
|
10
|
-
APP_STORE_URL: JSON.stringify(
|
|
11
|
-
AD_NETWORK: JSON.stringify(
|
|
12
|
-
AD_PROTOCOL: JSON.stringify(
|
|
13
|
-
APP: JSON.stringify(
|
|
14
|
-
NAME: JSON.stringify(
|
|
15
|
-
VERSION: JSON.stringify(
|
|
10
|
+
__DEV__: JSON.stringify(opts['dev'] === undefined ? false : opts['dev']),
|
|
11
|
+
GOOGLE_PLAY_URL: JSON.stringify(opts.googlePlayUrl),
|
|
12
|
+
APP_STORE_URL: JSON.stringify(opts.appStoreUrl),
|
|
13
|
+
AD_NETWORK: JSON.stringify(opts['network']),
|
|
14
|
+
AD_PROTOCOL: JSON.stringify(opts['protocol']),
|
|
15
|
+
APP: JSON.stringify(opts.app),
|
|
16
|
+
NAME: JSON.stringify(opts.name),
|
|
17
|
+
VERSION: JSON.stringify(opts.version),
|
|
16
18
|
BUILD_HASH: JSON.stringify((+new Date()).toString(36)),
|
|
17
|
-
LANGUAGE: JSON.stringify(
|
|
18
|
-
|
|
19
|
+
LANGUAGE: JSON.stringify(opts.language),
|
|
20
|
+
PLAYCRAFT_ORIENTATION: JSON.stringify(opts.playcraftOrientationValue || 'both')
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
/** @type {AD_NETWORK} */
|
|
22
|
-
let adNetworkDefine =
|
|
24
|
+
let adNetworkDefine = opts['network'];
|
|
23
25
|
|
|
24
26
|
if (adNetworkDefine === 'pangle') adNetworkDefine = 'tiktok';
|
|
25
27
|
if (adNetworkDefine === 'mytarget' || adNetworkDefine === 'moloco') adNetworkDefine = 'facebook';
|
|
@@ -33,7 +33,6 @@ exports.buildTemplateString = function buildTemplateString(template, customOptio
|
|
|
33
33
|
template = template.replaceAll('{version}', customOptions.version);
|
|
34
34
|
template = template.replaceAll('{date}', getCurrentDateFormatted());
|
|
35
35
|
template = template.replaceAll('{language}', customOptions.language);
|
|
36
|
-
template = template.replaceAll('{orientation}', customOptions.orientation);
|
|
37
36
|
template = template.replaceAll('{network}', networkName.toLowerCase());
|
|
38
37
|
template = template.replaceAll('{hash}', '[fullhash:6]');
|
|
39
38
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 根据当前构建的横竖屏方向版本,计算用于产物文件夹/文件命名的后缀。
|
|
3
|
+
* 命名规则:landscape → '_L',portrait → '_P',both(自适应)→ ''(无后缀)。
|
|
4
|
+
* @param {'both'|'landscape'|'portrait'} [orientation] - 方向版本,默认为 'both'
|
|
5
|
+
* @returns {string} 命名后缀
|
|
6
|
+
*/
|
|
7
|
+
function getPlaycraftOrientationSuffix(orientation) {
|
|
8
|
+
if (orientation === 'landscape') return '_L';
|
|
9
|
+
if (orientation === 'portrait') return '_P';
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
exports.getPlaycraftOrientationSuffix = getPlaycraftOrientationSuffix;
|
|
@@ -22,14 +22,14 @@ const { SocialMediaInjectorPlugin } = require('../plugins/SocialMediaInjectorPlu
|
|
|
22
22
|
* @param {Object} options - 配置选项
|
|
23
23
|
* @param {AD_NETWORK} options.network - 广告网络
|
|
24
24
|
* @param {AD_PROTOCOL} options.protocol - 广告协议
|
|
25
|
-
* @param {
|
|
25
|
+
* @param {'both'|'landscape'|'portrait'} [options.playcraftOrientationValue] - 当前构建的横竖屏方向版本(google 渠道需要)
|
|
26
26
|
* @param {string} [options.projectRoot] - 项目根目录(social_media 渠道读取 theme.data.json5 用)
|
|
27
27
|
* @param {string} [options.themeName] - 当前主题名
|
|
28
28
|
* @param {object} [options.themeConfig] - builds.config.js 中的 themes 配置
|
|
29
29
|
* @param {object} [options.mergedData] - 预合并的主题数据(可选)
|
|
30
30
|
*/
|
|
31
31
|
function injectSDKPlugins(webpackConfig, options) {
|
|
32
|
-
const { network: adNetwork, protocol: adProtocol,
|
|
32
|
+
const { network: adNetwork, protocol: adProtocol, playcraftOrientationValue, projectRoot, themeName, themeConfig, mergedData } = options;
|
|
33
33
|
|
|
34
34
|
// 协议层注入
|
|
35
35
|
if ('dapi' === adProtocol) {
|
|
@@ -44,7 +44,7 @@ function injectSDKPlugins(webpackConfig, options) {
|
|
|
44
44
|
|
|
45
45
|
// 渠道特定注入
|
|
46
46
|
if (adNetwork === 'google') {
|
|
47
|
-
webpackConfig.plugins.push(new ExitAPIInjectorPlugin(
|
|
47
|
+
webpackConfig.plugins.push(new ExitAPIInjectorPlugin(playcraftOrientationValue));
|
|
48
48
|
} else if (adNetwork === 'unity') {
|
|
49
49
|
// Unity Ads 使用专门的 UnityInjectorPlugin
|
|
50
50
|
webpackConfig.plugins.push(new UnityInjectorPlugin());
|
package/core/utils/logOptions.js
CHANGED
|
@@ -22,7 +22,16 @@ exports.logOptions = function logOptions(options) {
|
|
|
22
22
|
if (logOptions.jsConfig === 'jsconfig.json') delete logOptions.jsConfig;
|
|
23
23
|
if (logOptions.buildConfig === 'build.json') delete logOptions.buildConfig;
|
|
24
24
|
if (logOptions.language === 'en') delete logOptions.language;
|
|
25
|
-
|
|
25
|
+
// playcraftOrientation/playcraftOrientationChannel 为默认值时不显示,避免日志噪音
|
|
26
|
+
if (Array.isArray(logOptions.playcraftOrientation) && logOptions.playcraftOrientation.length === 1 && logOptions.playcraftOrientation[0] === 'both') {
|
|
27
|
+
delete logOptions.playcraftOrientation;
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(logOptions.playcraftOrientationChannel) && logOptions.playcraftOrientationChannel.length === 0) {
|
|
30
|
+
delete logOptions.playcraftOrientationChannel;
|
|
31
|
+
}
|
|
32
|
+
if (!logOptions.playcraftOrientationValue || logOptions.playcraftOrientationValue === 'both') {
|
|
33
|
+
delete logOptions.playcraftOrientationValue;
|
|
34
|
+
}
|
|
26
35
|
// 在 dev 模式下,如果 network 为默认值 preview 则不显示
|
|
27
36
|
if (!isProduction && logOptions.network === 'preview') delete logOptions.network;
|
|
28
37
|
// 在 dev 模式下,如果 protocol 为默认值 none 则不显示
|
|
@@ -46,8 +46,8 @@ const mraidPartners = [
|
|
|
46
46
|
/** @type {LANGUAGE[]} */
|
|
47
47
|
const allowedLanguages = ['auto', 'en', 'es', 'zh', 'hi', 'ar', 'fr', 'de', 'ja', 'pt'];
|
|
48
48
|
|
|
49
|
-
/** @type {
|
|
50
|
-
const
|
|
49
|
+
/** @type {('both'|'landscape'|'portrait')[]} playcraftOrientation 允许的横竖屏方向版本取值 */
|
|
50
|
+
const allowedPlaycraftOrientations = ['both', 'landscape', 'portrait'];
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Parses command line arguments based on provided options configuration
|
|
@@ -111,4 +111,4 @@ exports.allowedAdNetworks = allowedAdNetworks;
|
|
|
111
111
|
exports.allowedAdProtocols = allowedAdProtocols;
|
|
112
112
|
exports.mraidPartners = mraidPartners;
|
|
113
113
|
exports.allowedLanguages = allowedLanguages;
|
|
114
|
-
exports.
|
|
114
|
+
exports.allowedPlaycraftOrientations = allowedPlaycraftOrientations;
|
package/core/webpack.build.js
CHANGED
|
@@ -19,6 +19,8 @@ const { buildTemplateString } = require('./utils/buildTemplateString.js');
|
|
|
19
19
|
const { logOptions } = require('./utils/logOptions.js');
|
|
20
20
|
const { injectSDKPlugins } = require('./utils/injectSDKPlugins.js');
|
|
21
21
|
const { resolveChannelFold2Zip } = require('./utils/resolveChannelFold2Zip.js');
|
|
22
|
+
const { getPlaycraftOrientationSuffix } = require('./utils/getPlaycraftOrientationSuffix.js');
|
|
23
|
+
const { PlaycraftOrientationInjectorPlugin } = require('./plugins/PlaycraftOrientationInjectorPlugin.js');
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
const META = {
|
|
@@ -106,6 +108,13 @@ function makeWebpackBuildConfig(customOptions, customDefines, webpackCustomConfi
|
|
|
106
108
|
/** @type {AD_PROTOCOL} */
|
|
107
109
|
const adProtocol = buildOptions['protocol'];
|
|
108
110
|
|
|
111
|
+
/** @type {'both'|'landscape'|'portrait'} 当前单次构建实际使用的横竖屏方向版本,默认 'both' */
|
|
112
|
+
const playcraftOrientationValue = buildOptions.playcraftOrientationValue || 'both';
|
|
113
|
+
/** @type {string} 方向版本对应的命名后缀:landscape→'_L',portrait→'_P',both→'' */
|
|
114
|
+
const orientationSuffix = getPlaycraftOrientationSuffix(playcraftOrientationValue);
|
|
115
|
+
/** @type {string} 渠道 + 方向后缀组成的产物标识,如 'google_L' */
|
|
116
|
+
const outputId = `${adNetwork}${orientationSuffix}`;
|
|
117
|
+
|
|
109
118
|
let outDir = buildTemplateString(buildOptions.outDir, buildOptions);
|
|
110
119
|
|
|
111
120
|
function getFileName() {
|
|
@@ -224,9 +233,11 @@ function makeWebpackBuildConfig(customOptions, customDefines, webpackCustomConfi
|
|
|
224
233
|
plugins: [
|
|
225
234
|
new HtmlWebpackPlugin(htmlWebpackPluginConfig),
|
|
226
235
|
new MetaTagOverridePlugin(adNetwork),
|
|
236
|
+
// 注入 window.playcraftOrientation 全局变量,标识当前构建产物对应的横竖屏方向版本
|
|
237
|
+
new PlaycraftOrientationInjectorPlugin(playcraftOrientationValue),
|
|
227
238
|
|
|
228
239
|
new webpack.DefinePlugin({
|
|
229
|
-
...buildDefines(),
|
|
240
|
+
...buildDefines(buildOptions),
|
|
230
241
|
...buildOptions.defines,
|
|
231
242
|
...customDefines
|
|
232
243
|
}),
|
|
@@ -388,7 +399,7 @@ function makeWebpackBuildConfig(customOptions, customDefines, webpackCustomConfi
|
|
|
388
399
|
injectSDKPlugins(webpackConfig, {
|
|
389
400
|
network: adNetwork,
|
|
390
401
|
protocol: adProtocol,
|
|
391
|
-
|
|
402
|
+
playcraftOrientationValue
|
|
392
403
|
});
|
|
393
404
|
|
|
394
405
|
if (isZipOutputFinal) {
|
|
@@ -418,13 +429,13 @@ function makeWebpackBuildConfig(customOptions, customDefines, webpackCustomConfi
|
|
|
418
429
|
|
|
419
430
|
webpackConfig.plugins.push(
|
|
420
431
|
new ZipPlugin({
|
|
421
|
-
filename: isChannelFold2Zip ?
|
|
432
|
+
filename: isChannelFold2Zip ? outputId : getFileName(),
|
|
422
433
|
path: path.resolve(outDir),
|
|
423
434
|
cleanTempFolder: true // 🗑️ 生成 zip 后删除临时文件夹
|
|
424
435
|
})
|
|
425
436
|
);
|
|
426
437
|
|
|
427
|
-
webpackConfig.output.path = path.resolve(outDir,
|
|
438
|
+
webpackConfig.output.path = path.resolve(outDir, outputId);
|
|
428
439
|
if (adNetwork === 'adikteev') {
|
|
429
440
|
webpackConfig.output.filename = 'creative.js';
|
|
430
441
|
// Adikteev 需要将 CSS 单独提取为 style.css 文件
|
|
@@ -467,13 +478,33 @@ function runBuild(webpackConfig, customOptions, customDefines, webpackCustomConf
|
|
|
467
478
|
if (!webpackConfig) webpackConfig = makeWebpackBuildConfig(customOptions, customDefines, webpackCustomConfig);
|
|
468
479
|
|
|
469
480
|
const compiler = webpack(webpackConfig);
|
|
481
|
+
|
|
482
|
+
// 必须显式调用 compiler.close() 释放本次编译占用的内存(模块图/资源缓存/
|
|
483
|
+
// Terser 与 obfuscator 等插件持有的引用)。webpack 官方要求:若在同一进程内
|
|
484
|
+
// 多次调用 compiler.run(),每次 run 完成后都要 close,否则上一次编译的内存
|
|
485
|
+
// 无法被回收。playcraftOrientationChannel 会在同一进程里串行构建多个方向
|
|
486
|
+
// 版本(多次调用本函数),若不 close,内存会随构建次数线性累加,最终触发
|
|
487
|
+
// "JavaScript heap out of memory"。
|
|
488
|
+
const finalize = (error) => {
|
|
489
|
+
compiler.close((closeErr) => {
|
|
490
|
+
if (closeErr) {
|
|
491
|
+
console.error('关闭 webpack compiler 时出错:', closeErr);
|
|
492
|
+
}
|
|
493
|
+
if (error) {
|
|
494
|
+
reject(error);
|
|
495
|
+
} else {
|
|
496
|
+
resolve();
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
};
|
|
500
|
+
|
|
470
501
|
compiler.run((err, stats) => {
|
|
471
502
|
if (err) {
|
|
472
503
|
console.error('Build failed:', err.stack || err);
|
|
473
504
|
if (err.details) {
|
|
474
505
|
console.error('Error details:', err.details);
|
|
475
506
|
}
|
|
476
|
-
return
|
|
507
|
+
return finalize(err);
|
|
477
508
|
}
|
|
478
509
|
|
|
479
510
|
if (stats.hasErrors()) {
|
|
@@ -491,10 +522,10 @@ function runBuild(webpackConfig, customOptions, customDefines, webpackCustomConf
|
|
|
491
522
|
|
|
492
523
|
// 拒绝 Promise 时传递格式化的错误消息
|
|
493
524
|
const errorMessage = errors.map((e, i) => `[${i + 1}] ${e.message || e}`).join('\n');
|
|
494
|
-
|
|
525
|
+
finalize(new Error(`Webpack build failed with ${errors.length} error(s):\n${errorMessage}`));
|
|
495
526
|
} else {
|
|
496
527
|
console.log(`Build successful!`);
|
|
497
|
-
|
|
528
|
+
finalize(null);
|
|
498
529
|
}
|
|
499
530
|
});
|
|
500
531
|
});
|
package/core/webpack.dev.js
CHANGED
|
@@ -10,6 +10,7 @@ const { mergeOptions } = require('./utils/mergeOptions.js');
|
|
|
10
10
|
const { buildDefines } = require('./utils/buildDefines.js');
|
|
11
11
|
const { logOptions } = require('./utils/logOptions.js');
|
|
12
12
|
const { injectSDKPlugins } = require('./utils/injectSDKPlugins.js');
|
|
13
|
+
const { PlaycraftOrientationInjectorPlugin } = require('./plugins/PlaycraftOrientationInjectorPlugin.js');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Creates webpack configuration for development
|
|
@@ -24,6 +25,9 @@ function makeWebpackDevConfig(customOptions, customDefines, webpackCustomConfig)
|
|
|
24
25
|
customDefines = customDefines || {};
|
|
25
26
|
webpackCustomConfig = webpackCustomConfig || {};
|
|
26
27
|
|
|
28
|
+
/** @type {'both'|'landscape'|'portrait'} */
|
|
29
|
+
const playcraftOrientationValue = devOptions.playcraftOrientationValue || 'both';
|
|
30
|
+
|
|
27
31
|
const webpackConfig = merge(
|
|
28
32
|
webpackCommonConfig,
|
|
29
33
|
{
|
|
@@ -47,9 +51,11 @@ function makeWebpackDevConfig(customOptions, customDefines, webpackCustomConfig)
|
|
|
47
51
|
viewport: 'width=device-width,initial-scale=1.0,viewport-fit=cover,maximum-scale=1.0,user-scalable=no'
|
|
48
52
|
}
|
|
49
53
|
}),
|
|
54
|
+
// 注入 window.playcraftOrientation 全局变量,标识当前构建产物对应的横竖屏方向版本
|
|
55
|
+
new PlaycraftOrientationInjectorPlugin(playcraftOrientationValue),
|
|
50
56
|
|
|
51
57
|
new webpack.DefinePlugin({
|
|
52
|
-
...buildDefines(),
|
|
58
|
+
...buildDefines(devOptions),
|
|
53
59
|
...devOptions.defines,
|
|
54
60
|
__DEV__: JSON.stringify(devOptions['dev'] === undefined ? true : devOptions['dev']),
|
|
55
61
|
...customDefines
|
|
@@ -67,7 +73,7 @@ function makeWebpackDevConfig(customOptions, customDefines, webpackCustomConfig)
|
|
|
67
73
|
injectSDKPlugins(webpackConfig, {
|
|
68
74
|
network: devOptions['network'],
|
|
69
75
|
protocol: devOptions['protocol'],
|
|
70
|
-
|
|
76
|
+
playcraftOrientationValue
|
|
71
77
|
});
|
|
72
78
|
|
|
73
79
|
return webpackConfig;
|
package/defines.d.ts
CHANGED
|
@@ -49,7 +49,7 @@ declare const APP: string;
|
|
|
49
49
|
declare const NAME: string;
|
|
50
50
|
declare const VERSION: string;
|
|
51
51
|
declare const LANGUAGE: 'auto' | 'en' | 'es' | 'zh' | 'hi' | 'ar' | 'fr' | 'de' | 'ja' | 'pt';
|
|
52
|
-
declare const
|
|
52
|
+
declare const PLAYCRAFT_ORIENTATION: 'both' | 'landscape' | 'portrait';
|
|
53
53
|
declare const BUILD_HASH: string;
|
|
54
54
|
|
|
55
55
|
declare const __DEV__: boolean;
|