@playcraft/devkit 1.0.16 → 1.0.17
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 +259 -0
- package/cli/bin/playable-scripts.js +44 -1
- package/cli/commands/build.js +122 -17
- package/cli/commands/builds.js +13 -2
- package/cli/commands/dev.js +7 -1
- package/cli/commands/pack.js +308 -39
- package/cli/commands/vite-dev.js +7 -1
- package/core/batch-build.js +1091 -124
- package/core/cos-uploader.js +264 -20
- package/core/index.js +58 -4
- package/core/loaders/gltf-loader.js +74 -28
- package/core/options.js +310 -15
- package/core/plugins/AdikteevInjectorPlugin.js +26 -4
- package/core/plugins/BigoAdsInjectorPlugin.js +21 -4
- package/core/plugins/DAPIInjectorPlugin.js +25 -2
- package/core/plugins/DebuggerInjectionPlugin.js +31 -3
- package/core/plugins/ExitAPIInjectorPlugin.js +57 -3
- package/core/plugins/FflateCompressionPlugin.js +225 -37
- package/core/plugins/LiftoffInjectorPlugin.js +26 -4
- package/core/plugins/MRAIDInjectorPlugin.js +24 -2
- package/core/plugins/MintegralInjectorPlugin.js +25 -2
- package/core/plugins/PangleInjectorPlugin.js +24 -2
- package/core/plugins/SnapchatInjectorPlugin.js +26 -4
- package/core/plugins/TikTokInjectorPlugin.js +24 -2
- package/core/plugins/UnityInjectorPlugin.js +37 -8
- package/core/plugins/ZipPlugin.js +138 -19
- package/core/utils/buildDefines.js +29 -2
- package/core/utils/buildTemplateString.js +40 -5
- package/core/utils/date.js +16 -1
- package/core/utils/generateAdikteevHtmlWebpackPluginConfig.js +42 -7
- package/core/utils/generateBigabidHtmlWebpackPluginConfig.js +30 -3
- package/core/utils/generateInMobiHtmlWebpackPluginConfig.js +43 -7
- package/core/utils/injectSDKPlugins.js +58 -11
- package/core/utils/logOptions.js +37 -4
- package/core/utils/mergeOptions.js +19 -2
- package/core/utils/parseArgvOptions.js +110 -5
- package/core/utils/resolveChannelFold2Zip.js +15 -3
- package/core/utils/validateThemeData.js +220 -25
- package/core/validators/pre-build-checker.js +201 -21
- package/core/validators/tracking-validator.js +358 -40
- package/core/vite.dev.js +181 -15
- package/core/webpack.build.js +446 -52
- package/core/webpack.common.js +177 -11
- package/core/webpack.dev.js +82 -5
- package/package.json +3 -2
|
@@ -1,19 +1,138 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const ModuleFilenameHelpers = require('webpack/lib/ModuleFilenameHelpers');
|
|
6
|
+
const webpack = require('webpack');
|
|
7
|
+
const webpackSources = webpack.sources || require('webpack-sources');
|
|
8
|
+
const yazl = require('yazl');
|
|
9
|
+
|
|
10
|
+
function ZipPlugin(options) {
|
|
11
|
+
this.options = options || {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
ZipPlugin.prototype.apply = function (compiler) {
|
|
15
|
+
const options = this.options;
|
|
16
|
+
const isWebpack4 = webpack.version.startsWith('4.');
|
|
17
|
+
// Prefer `compiler.webpack` instead of webpack or webpack-sources import (rspack compatibility hack)
|
|
18
|
+
const { RawSource } = (compiler.webpack && compiler.webpack.sources) || webpackSources;
|
|
19
|
+
|
|
20
|
+
if (options.pathPrefix && path.isAbsolute(options.pathPrefix)) {
|
|
21
|
+
throw new Error('"pathPrefix" must be a relative path');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 保存临时文件夹路径,用于后续删除
|
|
25
|
+
let tempFolderToClean = null;
|
|
26
|
+
|
|
27
|
+
const process = function (compilation, callback) {
|
|
28
|
+
// assets from child compilers will be included in the parent
|
|
29
|
+
// so we should not run in child compilers
|
|
30
|
+
if (compilation.compiler.isChild()) {
|
|
31
|
+
callback();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const zipFile = new yazl.ZipFile();
|
|
36
|
+
|
|
37
|
+
const pathPrefix = options.pathPrefix || '';
|
|
38
|
+
const pathMapper =
|
|
39
|
+
options.pathMapper ||
|
|
40
|
+
function (x) {
|
|
41
|
+
return x;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// populate the zip file with each asset
|
|
45
|
+
for (const nameAndPath of Object.keys(compilation.assets)) {
|
|
46
|
+
// match against include and exclude, which may be strings, regexes, arrays of the previous or omitted
|
|
47
|
+
if (!ModuleFilenameHelpers.matchObject({ include: options.include, exclude: options.exclude }, nameAndPath)) continue;
|
|
48
|
+
|
|
49
|
+
const source = compilation.assets[nameAndPath].source();
|
|
50
|
+
|
|
51
|
+
zipFile.addBuffer(
|
|
52
|
+
Buffer.isBuffer(source) ? source : Buffer.from ? Buffer.from(source) : new Buffer(source),
|
|
53
|
+
path.join(pathPrefix, pathMapper(nameAndPath)),
|
|
54
|
+
options.fileOptions
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
zipFile.end(options.zipOptions);
|
|
59
|
+
|
|
60
|
+
// accumulate each buffer containing a part of the zip file
|
|
61
|
+
const bufs = [];
|
|
62
|
+
|
|
63
|
+
zipFile.outputStream.on('data', function (buf) {
|
|
64
|
+
bufs.push(buf);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
zipFile.outputStream.on('end', function () {
|
|
68
|
+
// default to webpack's root output path if no path provided
|
|
69
|
+
const outputPath = options.path || compilation.options.output.path;
|
|
70
|
+
// default to webpack root filename if no filename provided, else the basename of the output path
|
|
71
|
+
let outputFilename = options.filename || compilation.options.output.filename || path.basename(outputPath);
|
|
72
|
+
|
|
73
|
+
const fullHash = compilation.fullHash || compilation.hash;
|
|
74
|
+
outputFilename = outputFilename.replace('[fullhash:6]', fullHash.slice(0, 6));
|
|
75
|
+
|
|
76
|
+
const extension = '.' + (options.extension || 'zip');
|
|
77
|
+
|
|
78
|
+
// combine the output path and filename
|
|
79
|
+
const outputPathAndFilename = path.resolve(
|
|
80
|
+
compilation.options.output.path, // ...supporting both absolute and relative paths
|
|
81
|
+
outputPath,
|
|
82
|
+
path.basename(outputFilename, '.zip') + extension // ...and filenames with and without a .zip extension
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// resolve a relative output path with respect to webpack's root output path
|
|
86
|
+
// since only relative paths are permitted for keys in `compilation.assets`
|
|
87
|
+
const relativeOutputPath = path.relative(compilation.options.output.path, outputPathAndFilename);
|
|
88
|
+
|
|
89
|
+
// add our zip file to the assets
|
|
90
|
+
const zipFileSource = new RawSource(Buffer.concat(bufs));
|
|
91
|
+
if (isWebpack4) {
|
|
92
|
+
compilation.assets[relativeOutputPath] = zipFileSource;
|
|
93
|
+
} else {
|
|
94
|
+
compilation.emitAsset(relativeOutputPath, zipFileSource);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 🗑️ 保存临时文件夹路径,用于后续删除
|
|
98
|
+
if (options.cleanTempFolder !== false) {
|
|
99
|
+
const tempFolder = compilation.options.output.path;
|
|
100
|
+
const zipOutputPath = options.path || compilation.options.output.path;
|
|
101
|
+
// 确保临时文件夹存在且不是 zip 输出目录
|
|
102
|
+
if (tempFolder && tempFolder !== zipOutputPath) {
|
|
103
|
+
tempFolderToClean = tempFolder;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
callback();
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (isWebpack4) {
|
|
112
|
+
compiler.hooks.emit.tapAsync(ZipPlugin.name, process);
|
|
113
|
+
} else {
|
|
114
|
+
compiler.hooks.thisCompilation.tap(ZipPlugin.name, (compilation) => {
|
|
115
|
+
compilation.hooks.processAssets.tapPromise(
|
|
116
|
+
{
|
|
117
|
+
name: ZipPlugin.name,
|
|
118
|
+
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER
|
|
119
|
+
},
|
|
120
|
+
() => new Promise((resolve) => process(compilation, resolve))
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 🗑️ 在所有文件写入完成后删除临时文件夹
|
|
126
|
+
compiler.hooks.done.tap(ZipPlugin.name, () => {
|
|
127
|
+
if (tempFolderToClean && fs.existsSync(tempFolderToClean)) {
|
|
128
|
+
try {
|
|
129
|
+
fs.rmSync(tempFolderToClean, { recursive: true, force: true });
|
|
130
|
+
console.log(`✅ 已删除临时文件夹: ${tempFolderToClean}`);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.warn(`⚠️ 删除临时文件夹失败: ${tempFolderToClean}`, err.message);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
exports.ZipPlugin = ZipPlugin;
|
|
@@ -1,3 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
const { options } = require('../options');
|
|
2
|
+
|
|
3
|
+
/**
|
|
2
4
|
* Build's default defines list for dev & prod builds
|
|
3
|
-
*/
|
|
5
|
+
*/
|
|
6
|
+
exports.buildDefines = function buildDefines() {
|
|
7
|
+
const defines = {
|
|
8
|
+
__DEV__: JSON.stringify(options['dev'] === undefined ? false : options['dev']),
|
|
9
|
+
GOOGLE_PLAY_URL: JSON.stringify(options.googlePlayUrl),
|
|
10
|
+
APP_STORE_URL: JSON.stringify(options.appStoreUrl),
|
|
11
|
+
AD_NETWORK: JSON.stringify(options['network']),
|
|
12
|
+
AD_PROTOCOL: JSON.stringify(options['protocol']),
|
|
13
|
+
APP: JSON.stringify(options.app),
|
|
14
|
+
NAME: JSON.stringify(options.name),
|
|
15
|
+
VERSION: JSON.stringify(options.version),
|
|
16
|
+
BUILD_HASH: JSON.stringify((+new Date()).toString(36)),
|
|
17
|
+
LANGUAGE: JSON.stringify(options.language),
|
|
18
|
+
ORIENTATION: JSON.stringify(options.orientation)
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** @type {AD_NETWORK} */
|
|
22
|
+
let adNetworkDefine = options['network'];
|
|
23
|
+
|
|
24
|
+
if (adNetworkDefine === 'pangle') adNetworkDefine = 'tiktok';
|
|
25
|
+
if (adNetworkDefine === 'mytarget' || adNetworkDefine === 'moloco') adNetworkDefine = 'facebook';
|
|
26
|
+
|
|
27
|
+
defines['AD_NETWORK'] = JSON.stringify(adNetworkDefine);
|
|
28
|
+
|
|
29
|
+
return defines;
|
|
30
|
+
};
|
|
@@ -1,6 +1,41 @@
|
|
|
1
|
-
|
|
1
|
+
const { options } = require('../options');
|
|
2
|
+
const { getCurrentDateFormatted } = require('./date');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/** @type {Record<string, string>} Mapping of ad network identifiers to their display names in filenames */
|
|
6
|
+
const adNetworkFileNameMap = {
|
|
7
|
+
preview: 'Preview',
|
|
8
|
+
bigoads: 'bigo'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
2
12
|
* Build's result string from template. Used for outDir or filename
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
13
|
+
*/
|
|
14
|
+
exports.buildTemplateString = function buildTemplateString(template, customOptions) {
|
|
15
|
+
customOptions = customOptions || options;
|
|
16
|
+
|
|
17
|
+
/** @type {AD_NETWORK} */
|
|
18
|
+
const adNetwork = customOptions['network'];
|
|
19
|
+
|
|
20
|
+
/** @type {AD_PROTOCOL} */
|
|
21
|
+
// const adProtocol = customOptions['protocol'];
|
|
22
|
+
|
|
23
|
+
let networkName = customOptions.adNetworkNames[adNetwork] || adNetworkFileNameMap[adNetwork] || adNetwork;
|
|
24
|
+
// if (adProtocol === 'dapi') networkName += '_DAPI';
|
|
25
|
+
|
|
26
|
+
// 如果是绝对路径,直接返回,不做模板替换
|
|
27
|
+
if (path.isAbsolute(template)) {
|
|
28
|
+
return template;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
template = template.replaceAll('{app}', customOptions.app);
|
|
32
|
+
template = template.replaceAll('{name}', customOptions.name);
|
|
33
|
+
template = template.replaceAll('{version}', customOptions.version);
|
|
34
|
+
template = template.replaceAll('{date}', getCurrentDateFormatted());
|
|
35
|
+
template = template.replaceAll('{language}', customOptions.language);
|
|
36
|
+
template = template.replaceAll('{orientation}', customOptions.orientation);
|
|
37
|
+
template = template.replaceAll('{network}', networkName.toLowerCase());
|
|
38
|
+
template = template.replaceAll('{hash}', '[fullhash:6]');
|
|
39
|
+
|
|
40
|
+
return template;
|
|
41
|
+
};
|
package/core/utils/date.js
CHANGED
|
@@ -1 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
exports.getCurrentDateFormatted = function getCurrentDateFormatted(includeTime) {
|
|
2
|
+
const date = new Date();
|
|
3
|
+
const year = date.getFullYear();
|
|
4
|
+
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
5
|
+
const day = date.getDate().toString().padStart(2, '0');
|
|
6
|
+
|
|
7
|
+
if (includeTime) {
|
|
8
|
+
const hour = date.getHours().toString().padStart(2, '0');
|
|
9
|
+
const minute = date.getMinutes().toString().padStart(2, '0');
|
|
10
|
+
const second = date.getSeconds().toString().padStart(2, '0');
|
|
11
|
+
|
|
12
|
+
return `${year}${month}${day}-${hour}${minute}${second}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return `${year}${month}${day}`;
|
|
16
|
+
};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
2
5
|
* Adikteev 平台要求的占位符变量
|
|
3
6
|
* 这些变量会在生产环境中被 Adikteev 平台替换为实际值
|
|
4
7
|
*
|
|
@@ -9,13 +12,16 @@
|
|
|
9
12
|
* 2. CSS 必须单独提取为 style.css,通过 <link rel="stylesheet"> 引用
|
|
10
13
|
* 3. JS 必须单独为 creative.js,通过 <script src> 引用
|
|
11
14
|
* 4. JS 和 CSS 文件会被上传到 Adikteev CDN
|
|
12
|
-
*/
|
|
15
|
+
*/
|
|
16
|
+
const AdikteevPlaceholders = `<meta charset="UTF-8">
|
|
13
17
|
<link rel="stylesheet" href="style.css">
|
|
14
18
|
<script type="application/javascript">
|
|
15
19
|
var AK_CLICK_DESTINATION_URL = "PLACEHOLDER_CLICK_REDIRECT";
|
|
16
20
|
var AK_CLICK_PIXEL_URL = "PLACEHOLDER_CLICK_PIXEL";
|
|
17
21
|
</script>
|
|
18
|
-
<script type="application/javascript" src="creative.js"></script
|
|
22
|
+
<script type="application/javascript" src="creative.js"></script>`;
|
|
23
|
+
|
|
24
|
+
/**
|
|
19
25
|
* 生成 Adikteev 平台所需的 HTML 配置
|
|
20
26
|
*
|
|
21
27
|
* Adikteev 特殊要求:
|
|
@@ -30,7 +36,36 @@
|
|
|
30
36
|
* - style.css → 上传到 Adikteev CDN
|
|
31
37
|
* - creative.js → 上传到 Adikteev CDN
|
|
32
38
|
* - index.html → HTML 片段(引用上述文件)
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
*/
|
|
40
|
+
exports.generateAdikteevHtmlWebpackPluginConfig = function generateAdikteevHtmlWebpackPluginConfig(originalHtmlContentPath) {
|
|
41
|
+
originalHtmlContentPath = originalHtmlContentPath || 'src/index.html';
|
|
42
|
+
let originalBody = '';
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const content = fs.readFileSync(path.resolve(originalHtmlContentPath), 'utf8');
|
|
46
|
+
const match = content.match(/<body>([\s\S]*?)<\/body>/);
|
|
47
|
+
|
|
48
|
+
if (match) {
|
|
49
|
+
originalBody = match[1].trim();
|
|
50
|
+
// 移除缩进以获得更清晰的输出
|
|
51
|
+
originalBody = originalBody.replaceAll(' ', '').replaceAll('\t', '');
|
|
52
|
+
}
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.warn('[Adikteev] Could not read original HTML body:', e.message);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
filename: 'index.html',
|
|
59
|
+
inject: false,
|
|
60
|
+
minify: false,
|
|
61
|
+
templateContent: ({ htmlWebpackPlugin }) => {
|
|
62
|
+
// 生成 HTML 代码片段(非完整文档)
|
|
63
|
+
// 结构: CSS 引用 → 占位符变量 → JS 引用 → body 内容
|
|
64
|
+
return [
|
|
65
|
+
AdikteevPlaceholders,
|
|
66
|
+
originalBody
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const BigabidPlaceholders = `<meta charset="UTF-8">
|
|
2
5
|
<script src="mraid.js"></script>
|
|
3
6
|
<script>window.BIGABID_BIDTIMEMACROS = {
|
|
4
7
|
mraid_viewable: unescape("#IMP_TRACE_MRAID_VIEWABLE_ESC#"),
|
|
@@ -9,5 +12,29 @@
|
|
|
9
12
|
final_url: unescape("#FINAL_LANDING_URL_ESC#")
|
|
10
13
|
}</script>
|
|
11
14
|
<script src="#BIGABID_PLAYABLE_CDN_URL#"></script>
|
|
12
|
-
%{IMP_BEACON}`;
|
|
13
|
-
|
|
15
|
+
%{IMP_BEACON}`;
|
|
16
|
+
|
|
17
|
+
exports.generateBigabidHtmlWebpackPluginConfig = function generateBigabidHtmlWebpackPluginConfig(originalHtmlContentPath) {
|
|
18
|
+
originalHtmlContentPath = originalHtmlContentPath || 'src/index.html';
|
|
19
|
+
let originalBody = '';
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const content = fs.readFileSync(path.resolve(originalHtmlContentPath), 'utf8');
|
|
23
|
+
const match = content.match(/<body>([\s\S]*?)<\/body>/);
|
|
24
|
+
|
|
25
|
+
if (match) {
|
|
26
|
+
originalBody = match[1].trim();
|
|
27
|
+
// Remove tabs for a better look
|
|
28
|
+
originalBody = originalBody.replaceAll(' ', '').replaceAll('\t', '');
|
|
29
|
+
}
|
|
30
|
+
} catch (e) {}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
filename: 'index.html',
|
|
34
|
+
inject: false,
|
|
35
|
+
minify: false,
|
|
36
|
+
templateContent: ({ htmlWebpackPlugin }) => {
|
|
37
|
+
return [BigabidPlaceholders, htmlWebpackPlugin.tags.headTags, originalBody].join('\n');
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
2
5
|
* InMobi 追踪宏定义
|
|
3
6
|
* 文档: https://support.inmobi.com/dsp/in/inmobi-dsp-india/creative-specifications-and-guidelines/
|
|
4
7
|
*
|
|
@@ -7,7 +10,8 @@
|
|
|
7
10
|
* 2. 必须包含 INMOBI_DSPMACROS 追踪变量
|
|
8
11
|
* 3. 引用外部 mraid.js(由 InMobi 平台提供)
|
|
9
12
|
* 4. 支持 MRAID 3.0 标准
|
|
10
|
-
*/
|
|
13
|
+
*/
|
|
14
|
+
const InMobiPlaceholders = `<script>
|
|
11
15
|
var INMOBI_DSPMACROS = {
|
|
12
16
|
"Ad_Load_Start": ["$P_AD_LOAD_START"],
|
|
13
17
|
"Ad_Viewable": ["$P_AD_READY"],
|
|
@@ -23,14 +27,46 @@
|
|
|
23
27
|
"Spent_30_Seconds": ["$P_TIMESPENT_30"]
|
|
24
28
|
};
|
|
25
29
|
</script>
|
|
26
|
-
<script src="mraid.js"></script
|
|
30
|
+
<script src="mraid.js"></script>`;
|
|
31
|
+
|
|
32
|
+
/**
|
|
27
33
|
* 生成 InMobi 平台所需的 HTML 配置
|
|
28
34
|
*
|
|
29
35
|
* 策略:
|
|
30
36
|
* 1. 使用标准的 HtmlWebpackPlugin 配置(不是 templateContent)
|
|
31
37
|
* 2. JS 会通过 HtmlInlineScriptPlugin 自动内联到 HTML 中
|
|
32
38
|
* 3. 通过修改 template 文件注入 InMobi 追踪脚本
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
*/
|
|
40
|
+
exports.generateInMobiHtmlWebpackPluginConfig = function generateInMobiHtmlWebpackPluginConfig(
|
|
41
|
+
originalHtmlContentPath,
|
|
42
|
+
buildOptions
|
|
43
|
+
) {
|
|
44
|
+
originalHtmlContentPath = originalHtmlContentPath || 'src/index.html';
|
|
45
|
+
|
|
46
|
+
const placeholder = InMobiPlaceholders
|
|
47
|
+
.replaceAll('{GOOGLE_PLAY_URL}', buildOptions.googlePlayUrl)
|
|
48
|
+
.replaceAll('{APP_STORE_URL}', buildOptions.appStoreUrl);
|
|
49
|
+
|
|
50
|
+
// 读取原始 HTML 模板
|
|
51
|
+
let htmlContent = '';
|
|
52
|
+
try {
|
|
53
|
+
htmlContent = fs.readFileSync(path.resolve(originalHtmlContentPath), 'utf8');
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.warn('[InMobi] Could not read HTML template:', e.message);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 在 </head> 之前注入 InMobi 追踪脚本
|
|
59
|
+
htmlContent = htmlContent.replace('</head>', `${placeholder}\n</head>`);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
filename: 'index.html',
|
|
63
|
+
inject: 'head', // webpack 会自动注入 JS,然后 HtmlInlineScriptPlugin 内联
|
|
64
|
+
minify: false,
|
|
65
|
+
templateContent: htmlContent,
|
|
66
|
+
meta: {
|
|
67
|
+
charset: 'UTF-8',
|
|
68
|
+
viewport: 'width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no'
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
@@ -1,19 +1,66 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* 根据 AD_NETWORK 和 AD_PROTOCOL 注入对应的渠道 SDK 脚本插件
|
|
3
3
|
* 此函数被 webpack.dev.js 和 webpack.build.js 共用
|
|
4
|
-
*/
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { ExitAPIInjectorPlugin } = require('../plugins/ExitAPIInjectorPlugin.js');
|
|
7
|
+
const { DAPIInjectorPlugin } = require('../plugins/DAPIInjectorPlugin.js');
|
|
8
|
+
const { PangleInjectorPlugin } = require('../plugins/PangleInjectorPlugin.js');
|
|
9
|
+
const { TikTokInjectorPlugin } = require('../plugins/TikTokInjectorPlugin.js');
|
|
10
|
+
const { MintegralInjectorPlugin } = require('../plugins/MintegralInjectorPlugin.js');
|
|
11
|
+
const { MRAIDInjectorPlugin } = require('../plugins/MRAIDInjectorPlugin.js');
|
|
12
|
+
const { BigoAdsInjectorPlugin } = require('../plugins/BigoAdsInjectorPlugin.js');
|
|
13
|
+
const { LiftoffInjectorPlugin } = require('../plugins/LiftoffInjectorPlugin.js');
|
|
14
|
+
const { SnapchatInjectorPlugin } = require('../plugins/SnapchatInjectorPlugin.js');
|
|
15
|
+
const { AdikteevInjectorPlugin } = require('../plugins/AdikteevInjectorPlugin.js');
|
|
16
|
+
const { UnityInjectorPlugin } = require('../plugins/UnityInjectorPlugin.js');
|
|
17
|
+
|
|
18
|
+
/**
|
|
5
19
|
* 注入渠道 SDK 插件到 webpack 配置中
|
|
6
20
|
* @param {import('webpack').Configuration} webpackConfig - webpack 配置对象
|
|
7
21
|
* @param {Object} options - 配置选项
|
|
8
22
|
* @param {AD_NETWORK} options.network - 广告网络
|
|
9
23
|
* @param {AD_PROTOCOL} options.protocol - 广告协议
|
|
10
24
|
* @param {string} [options.orientation] - 屏幕方向(google 渠道需要)
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
webpackConfig.plugins.push(new
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
*/
|
|
26
|
+
function injectSDKPlugins(webpackConfig, options) {
|
|
27
|
+
const { network: adNetwork, protocol: adProtocol, orientation } = options;
|
|
28
|
+
|
|
29
|
+
// 协议层注入
|
|
30
|
+
if ('dapi' === adProtocol) {
|
|
31
|
+
webpackConfig.plugins.push(new DAPIInjectorPlugin());
|
|
32
|
+
} else if ('mraid' === adProtocol) {
|
|
33
|
+
// 只有 ironsource 才注入 MRAIDInjectorPlugin
|
|
34
|
+
// Unity 使用专门的 UnityInjectorPlugin(在渠道特定注入中处理)
|
|
35
|
+
if (adNetwork === 'ironsource') {
|
|
36
|
+
webpackConfig.plugins.push(new MRAIDInjectorPlugin());
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 渠道特定注入
|
|
41
|
+
if (adNetwork === 'google') {
|
|
42
|
+
webpackConfig.plugins.push(new ExitAPIInjectorPlugin(orientation));
|
|
43
|
+
} else if (adNetwork === 'unity') {
|
|
44
|
+
// Unity Ads 使用专门的 UnityInjectorPlugin
|
|
45
|
+
webpackConfig.plugins.push(new UnityInjectorPlugin());
|
|
46
|
+
} else if (adNetwork === 'pangle') {
|
|
47
|
+
webpackConfig.plugins.push(new PangleInjectorPlugin());
|
|
48
|
+
} else if (adNetwork === 'tiktok') {
|
|
49
|
+
webpackConfig.plugins.push(new TikTokInjectorPlugin());
|
|
50
|
+
} else if (adNetwork === 'mintegral') {
|
|
51
|
+
webpackConfig.plugins.push(new MintegralInjectorPlugin());
|
|
52
|
+
} else if (adNetwork === 'bigoads') {
|
|
53
|
+
// BigoAds 在 dev 模式下注入模拟配置
|
|
54
|
+
webpackConfig.plugins.push(new BigoAdsInjectorPlugin());
|
|
55
|
+
} else if (adNetwork === 'liftoff') {
|
|
56
|
+
webpackConfig.plugins.push(new LiftoffInjectorPlugin());
|
|
57
|
+
} else if (adNetwork === 'snapchat') {
|
|
58
|
+
// Snapchat 注入 ScPlayableAd SDK
|
|
59
|
+
webpackConfig.plugins.push(new SnapchatInjectorPlugin());
|
|
60
|
+
} else if (adNetwork === 'adikteev') {
|
|
61
|
+
// Adikteev 注入 MRAID mock 和占位符变量
|
|
62
|
+
webpackConfig.plugins.push(new AdikteevInjectorPlugin());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
exports.injectSDKPlugins = injectSDKPlugins;
|
package/core/utils/logOptions.js
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
var prettyjson = require('prettyjson');
|
|
2
|
+
|
|
3
|
+
/**
|
|
2
4
|
* Log provided options into console output
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
*/
|
|
6
|
+
exports.logOptions = function logOptions(options) {
|
|
7
|
+
let logOptions = { mode: process.env.NODE_ENV, ...options };
|
|
8
|
+
let isProduction = false;
|
|
9
|
+
if (process.env.NODE_ENV === 'production') {
|
|
10
|
+
isProduction = true;
|
|
11
|
+
delete logOptions.port;
|
|
12
|
+
delete logOptions.open;
|
|
13
|
+
} else if (process.env.NODE_ENV === 'development') {
|
|
14
|
+
delete logOptions.outDir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (Object.keys(logOptions.compilation).length === 0 || !isProduction) delete logOptions.compilation;
|
|
18
|
+
if (Object.keys(logOptions.adNetworkNames).length === 0 || !isProduction) delete logOptions.adNetworkNames;
|
|
19
|
+
if (Object.keys(logOptions.defines).length === 0) delete logOptions.defines;
|
|
20
|
+
if (logOptions.zip === false || !isProduction) delete logOptions.zip;
|
|
21
|
+
if (logOptions.tsConfig === 'tsconfig.json') delete logOptions.tsConfig;
|
|
22
|
+
if (logOptions.jsConfig === 'jsconfig.json') delete logOptions.jsConfig;
|
|
23
|
+
if (logOptions.buildConfig === 'build.json') delete logOptions.buildConfig;
|
|
24
|
+
if (logOptions.language === 'en') delete logOptions.language;
|
|
25
|
+
if (logOptions.orientation === 'both') delete logOptions.orientation;
|
|
26
|
+
// 在 dev 模式下,如果 network 为默认值 preview 则不显示
|
|
27
|
+
if (!isProduction && logOptions.network === 'preview') delete logOptions.network;
|
|
28
|
+
// 在 dev 模式下,如果 protocol 为默认值 none 则不显示
|
|
29
|
+
if (!isProduction && logOptions.protocol === 'none') delete logOptions.protocol;
|
|
30
|
+
delete logOptions.filename;
|
|
31
|
+
delete logOptions.app;
|
|
32
|
+
delete logOptions.name;
|
|
33
|
+
delete logOptions.googlePlayUrl;
|
|
34
|
+
delete logOptions.appStoreUrl;
|
|
35
|
+
if (logOptions.obfuscateLevel === 2) delete logOptions.obfuscateLevel;
|
|
36
|
+
|
|
37
|
+
console.log(prettyjson.render(logOptions, {}, 2));
|
|
38
|
+
};
|
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* Merges two option objects with deep merge for 'defines' and 'build' properties
|
|
3
3
|
* @param {import('../index').CLIOptions} options1 - First options object
|
|
4
4
|
* @param {Partial<import('../index').CLIOptions>} [options2] - Second options object
|
|
5
5
|
* @returns {import('../index').CLIOptions} Merged options object with combined defines and build properties
|
|
6
|
-
*/
|
|
6
|
+
*/
|
|
7
|
+
exports.mergeOptions = function mergeOptions(options1, options2) {
|
|
8
|
+
if (!options1) return {};
|
|
9
|
+
if (!options2) return options1;
|
|
10
|
+
|
|
11
|
+
const defines = Object.assign({}, options1.defines, options2.defines);
|
|
12
|
+
const build = Object.assign({}, options1.build, options2.build);
|
|
13
|
+
const compilation = Object.assign({}, options1.compilation, options2.compilation);
|
|
14
|
+
const adNetworkNames = Object.assign({}, options1.adNetworkNames, options2.adNetworkNames);
|
|
15
|
+
const options = Object.assign({}, options1, options2);
|
|
16
|
+
|
|
17
|
+
options.defines = defines;
|
|
18
|
+
options.compilation = compilation;
|
|
19
|
+
options.adNetworkNames = adNetworkNames;
|
|
20
|
+
options.build = build;
|
|
21
|
+
|
|
22
|
+
return options;
|
|
23
|
+
};
|