@playcraft/build 0.0.48 → 0.0.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engines/playable-scripts-adapter.d.ts +9 -0
- package/dist/engines/playable-scripts-adapter.js +81 -8
- package/dist/platforms/bigo-lifecycle.d.ts +6 -0
- package/dist/platforms/bigo-lifecycle.js +35 -0
- package/dist/platforms/bigo.js +2 -0
- package/dist/platforms/unity.js +118 -26
- package/dist/validation/profiles.js +5 -6
- package/dist/validation/source-scanner.js +282 -20
- package/dist/vite/platform-configs.js +1 -1
- package/package.json +2 -2
|
@@ -15,8 +15,12 @@ export declare class PlayableScriptsAdapter {
|
|
|
15
15
|
private projectDir;
|
|
16
16
|
private options;
|
|
17
17
|
private config?;
|
|
18
|
+
private projectBuildsConfig?;
|
|
18
19
|
private static readonly DEFAULTS;
|
|
19
20
|
constructor(projectDir: string, options: BaseBuildOptions, config?: PlayableScriptsConfig);
|
|
21
|
+
private loadProjectBuildsConfig;
|
|
22
|
+
private resolveObfuscateLevel;
|
|
23
|
+
private resolveFflateCompression;
|
|
20
24
|
/**
|
|
21
25
|
* 执行 playable-scripts 构建
|
|
22
26
|
*
|
|
@@ -156,6 +160,11 @@ export declare class PlayableScriptsAdapter {
|
|
|
156
160
|
*/
|
|
157
161
|
private getPlatformFilename;
|
|
158
162
|
private runDevkitOutputValidation;
|
|
163
|
+
/**
|
|
164
|
+
* Devkit 产物会跳过 ViteBuilder,必须在这里补齐 PlayCraft 生命周期到渠道接口的桥接。
|
|
165
|
+
* 普通构建链由对应 PlatformAdapter 处理。
|
|
166
|
+
*/
|
|
167
|
+
private applyChannelLifecycleBridges;
|
|
159
168
|
private getValidationChannels;
|
|
160
169
|
private isSupportedValidationChannel;
|
|
161
170
|
private pickArtifactForChannel;
|
|
@@ -3,6 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { createRequire } from 'module';
|
|
6
|
+
import { strFromU8, strToU8, unzipSync, zipSync } from 'fflate';
|
|
7
|
+
import { injectBigoLifecycleBridge } from '../platforms/bigo-lifecycle.js';
|
|
6
8
|
import { formatValidationSummary, hasBlockingValidationErrors, runPlayableValidation, saveValidationReport, } from '../validation/index.js';
|
|
7
9
|
// ESM 兼容:获取当前模块的目录路径
|
|
8
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -20,7 +22,7 @@ const CHANNEL_MAP = {
|
|
|
20
22
|
/**
|
|
21
23
|
* PlayCraft 不支持的渠道(playable-scripts 不认识的 PlayCraft 平台)
|
|
22
24
|
*/
|
|
23
|
-
const UNSUPPORTED_CHANNELS = new Set(['remerge'
|
|
25
|
+
const UNSUPPORTED_CHANNELS = new Set(['remerge']);
|
|
24
26
|
/**
|
|
25
27
|
* 将 PlayCraft 平台名映射为 playable-scripts 渠道名
|
|
26
28
|
*/
|
|
@@ -49,6 +51,42 @@ export class PlayableScriptsAdapter {
|
|
|
49
51
|
console.log(`[PlayableScriptsAdapter] 目标渠道: ${config.channels.join(', ')}`);
|
|
50
52
|
}
|
|
51
53
|
}
|
|
54
|
+
loadProjectBuildsConfig(configPath) {
|
|
55
|
+
try {
|
|
56
|
+
const resolvedPath = require.resolve(configPath);
|
|
57
|
+
delete require.cache[resolvedPath];
|
|
58
|
+
const loaded = require(resolvedPath);
|
|
59
|
+
if (loaded && typeof loaded === 'object' && !Array.isArray(loaded)) {
|
|
60
|
+
return loaded;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.warn(`[PlayableScriptsAdapter] Failed to read builds.config.js build params, using PlayCraft defaults: ${error instanceof Error ? error.message : String(error)}`);
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
resolveObfuscateLevel() {
|
|
69
|
+
const explicitValue = this.config?.obfuscateLevel;
|
|
70
|
+
if (typeof explicitValue === 'number' && Number.isFinite(explicitValue)) {
|
|
71
|
+
return explicitValue;
|
|
72
|
+
}
|
|
73
|
+
const projectValue = this.projectBuildsConfig?.obfuscateLevel;
|
|
74
|
+
if (typeof projectValue === 'number' && Number.isFinite(projectValue)) {
|
|
75
|
+
return projectValue;
|
|
76
|
+
}
|
|
77
|
+
return PlayableScriptsAdapter.DEFAULTS.obfuscateLevel;
|
|
78
|
+
}
|
|
79
|
+
resolveFflateCompression() {
|
|
80
|
+
const explicitValue = this.config?.fflateCompression;
|
|
81
|
+
if (typeof explicitValue === 'boolean') {
|
|
82
|
+
return explicitValue;
|
|
83
|
+
}
|
|
84
|
+
const projectValue = this.projectBuildsConfig?.fflateCompression;
|
|
85
|
+
if (typeof projectValue === 'boolean') {
|
|
86
|
+
return projectValue;
|
|
87
|
+
}
|
|
88
|
+
return PlayableScriptsAdapter.DEFAULTS.fflateCompression;
|
|
89
|
+
}
|
|
52
90
|
/**
|
|
53
91
|
* 执行 playable-scripts 构建
|
|
54
92
|
*
|
|
@@ -93,6 +131,7 @@ export class PlayableScriptsAdapter {
|
|
|
93
131
|
const outputs = await this.collectOutputs();
|
|
94
132
|
// 7. 复制产物到 PlayCraft outputDir
|
|
95
133
|
const files = await this.copyToOutputDir(outputs);
|
|
134
|
+
await this.applyChannelLifecycleBridges(files);
|
|
96
135
|
await this.runDevkitOutputValidation(files);
|
|
97
136
|
// 7. 构建元数据
|
|
98
137
|
const metadata = {
|
|
@@ -149,10 +188,12 @@ export class PlayableScriptsAdapter {
|
|
|
149
188
|
let configContent;
|
|
150
189
|
try {
|
|
151
190
|
configContent = await fs.readFile(configPath, 'utf-8');
|
|
191
|
+
this.projectBuildsConfig = this.loadProjectBuildsConfig(configPath);
|
|
152
192
|
console.log('[PlayableScriptsAdapter] 读取现有 builds.config.js');
|
|
153
193
|
}
|
|
154
194
|
catch {
|
|
155
195
|
configContent = 'module.exports = {};';
|
|
196
|
+
this.projectBuildsConfig = undefined;
|
|
156
197
|
console.log('[PlayableScriptsAdapter] builds.config.js 不存在,将创建');
|
|
157
198
|
}
|
|
158
199
|
// 构建注入内容
|
|
@@ -225,11 +266,11 @@ export class PlayableScriptsAdapter {
|
|
|
225
266
|
buildCLIArgs() {
|
|
226
267
|
const config = this.config ?? {};
|
|
227
268
|
const args = [];
|
|
228
|
-
// 混淆等级:config 指定 > PlayCraft
|
|
229
|
-
const obfuscateLevel =
|
|
269
|
+
// 混淆等级:config 指定 > 项目 builds.config.js > PlayCraft 默认 4
|
|
270
|
+
const obfuscateLevel = this.resolveObfuscateLevel();
|
|
230
271
|
args.push('--obfuscate-level', `${obfuscateLevel}`);
|
|
231
|
-
// fflate 压缩:config 指定 > PlayCraft
|
|
232
|
-
const fflate =
|
|
272
|
+
// fflate 压缩:config 指定 > 项目 builds.config.js > PlayCraft 默认 true
|
|
273
|
+
const fflate = this.resolveFflateCompression();
|
|
233
274
|
args.push('--fflate-compression', `${fflate}`);
|
|
234
275
|
// 渠道列表
|
|
235
276
|
if (config.channels && config.channels.length > 0) {
|
|
@@ -280,8 +321,8 @@ export class PlayableScriptsAdapter {
|
|
|
280
321
|
// ==================== 3. 日志输出 ====================
|
|
281
322
|
logBuildParams() {
|
|
282
323
|
const config = this.config ?? {};
|
|
283
|
-
const obfuscateLevel =
|
|
284
|
-
const fflate =
|
|
324
|
+
const obfuscateLevel = this.resolveObfuscateLevel();
|
|
325
|
+
const fflate = this.resolveFflateCompression();
|
|
285
326
|
const levelLabels = {
|
|
286
327
|
1: 'minimum - 基础',
|
|
287
328
|
2: 'medium - 中等',
|
|
@@ -1282,6 +1323,34 @@ export class PlayableScriptsAdapter {
|
|
|
1282
1323
|
}
|
|
1283
1324
|
}
|
|
1284
1325
|
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Devkit 产物会跳过 ViteBuilder,必须在这里补齐 PlayCraft 生命周期到渠道接口的桥接。
|
|
1328
|
+
* 普通构建链由对应 PlatformAdapter 处理。
|
|
1329
|
+
*/
|
|
1330
|
+
async applyChannelLifecycleBridges(files) {
|
|
1331
|
+
const channels = this.getValidationChannels();
|
|
1332
|
+
if (!channels.includes('bigo')) {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
const artifacts = files.assets.filter((file) => file.endsWith('.html') || file.endsWith('.zip'));
|
|
1336
|
+
const artifact = this.pickArtifactForChannel('bigo', artifacts);
|
|
1337
|
+
if (!artifact) {
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (artifact.toLowerCase().endsWith('.html')) {
|
|
1341
|
+
const html = await fs.readFile(artifact, 'utf8');
|
|
1342
|
+
await fs.writeFile(artifact, injectBigoLifecycleBridge(html), 'utf8');
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
const archive = unzipSync(await fs.readFile(artifact));
|
|
1346
|
+
const htmlEntry = Object.keys(archive).find((entry) => /(?:^|\/)index\.html$/i.test(entry))
|
|
1347
|
+
?? Object.keys(archive).find((entry) => /\.html$/i.test(entry));
|
|
1348
|
+
if (!htmlEntry) {
|
|
1349
|
+
throw new Error(`[PlayableScriptsAdapter] BIGO ZIP artifact has no HTML entry: ${artifact}`);
|
|
1350
|
+
}
|
|
1351
|
+
archive[htmlEntry] = strToU8(injectBigoLifecycleBridge(strFromU8(archive[htmlEntry])));
|
|
1352
|
+
await fs.writeFile(artifact, zipSync(archive, { level: 9 }));
|
|
1353
|
+
}
|
|
1285
1354
|
getValidationChannels() {
|
|
1286
1355
|
if (this.options.validation?.channel) {
|
|
1287
1356
|
return [this.options.validation.channel];
|
|
@@ -1313,9 +1382,13 @@ export class PlayableScriptsAdapter {
|
|
|
1313
1382
|
if (artifacts.length === 1) {
|
|
1314
1383
|
return artifacts[0];
|
|
1315
1384
|
}
|
|
1385
|
+
const mappedChannel = mapChannel(channel).toLowerCase();
|
|
1316
1386
|
const direct = artifacts.find((artifact) => {
|
|
1317
1387
|
const normalized = artifact.replace(/\\/g, '/').toLowerCase();
|
|
1318
|
-
return normalized.includes(`/${channel}/`)
|
|
1388
|
+
return normalized.includes(`/${channel}/`)
|
|
1389
|
+
|| normalized.includes(`-${channel}.`)
|
|
1390
|
+
|| normalized.includes(`/${mappedChannel}/`)
|
|
1391
|
+
|| normalized.includes(`-${mappedChannel}.`);
|
|
1319
1392
|
});
|
|
1320
1393
|
return direct ?? artifacts[0];
|
|
1321
1394
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const BIGO_LIFECYCLE_BRIDGE_MARKER = "data-playcraft-bigo-lifecycle";
|
|
2
|
+
/**
|
|
3
|
+
* 把 PlayCraft Ads SDK (Advertising Software Development Kit, 广告软件开发工具包)
|
|
4
|
+
* 的 playcraftFinish 事件桥接到 BIGO 渠道结束接口。通过 marker 保证多条构建链重复处理时幂等。
|
|
5
|
+
*/
|
|
6
|
+
export declare function injectBigoLifecycleBridge(html: string): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const BIGO_LIFECYCLE_BRIDGE_MARKER = 'data-playcraft-bigo-lifecycle';
|
|
2
|
+
const BIGO_LIFECYCLE_BRIDGE = `<script ${BIGO_LIFECYCLE_BRIDGE_MARKER}>(function(){
|
|
3
|
+
var gameEndSent=false;
|
|
4
|
+
var bindAttempts=0;
|
|
5
|
+
function notifyGameEnd(){
|
|
6
|
+
if(gameEndSent)return;
|
|
7
|
+
if(!window.BGY_MRAID||typeof window.BGY_MRAID.gameEnd!=="function")return;
|
|
8
|
+
gameEndSent=true;
|
|
9
|
+
window.BGY_MRAID.gameEnd();
|
|
10
|
+
}
|
|
11
|
+
function bindPlayableSdk(){
|
|
12
|
+
var playableSdk=window.PlayableSDK;
|
|
13
|
+
if(playableSdk&&typeof playableSdk.once==="function"){
|
|
14
|
+
if(playableSdk.isFinished===true){notifyGameEnd();return;}
|
|
15
|
+
playableSdk.once("playcraftFinish",notifyGameEnd);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
bindAttempts+=1;
|
|
19
|
+
if(bindAttempts<300)window.setTimeout(bindPlayableSdk,100);
|
|
20
|
+
}
|
|
21
|
+
bindPlayableSdk();
|
|
22
|
+
})();</script>`;
|
|
23
|
+
/**
|
|
24
|
+
* 把 PlayCraft Ads SDK (Advertising Software Development Kit, 广告软件开发工具包)
|
|
25
|
+
* 的 playcraftFinish 事件桥接到 BIGO 渠道结束接口。通过 marker 保证多条构建链重复处理时幂等。
|
|
26
|
+
*/
|
|
27
|
+
export function injectBigoLifecycleBridge(html) {
|
|
28
|
+
if (html.includes(BIGO_LIFECYCLE_BRIDGE_MARKER)) {
|
|
29
|
+
return html;
|
|
30
|
+
}
|
|
31
|
+
if (/<\/head>/i.test(html)) {
|
|
32
|
+
return html.replace(/<\/head>/i, `${BIGO_LIFECYCLE_BRIDGE}</head>`);
|
|
33
|
+
}
|
|
34
|
+
return `${BIGO_LIFECYCLE_BRIDGE}${html}`;
|
|
35
|
+
}
|
package/dist/platforms/bigo.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PlatformAdapter } from './base.js';
|
|
2
|
+
import { injectBigoLifecycleBridge } from './bigo-lifecycle.js';
|
|
2
3
|
export class BigoAdapter extends PlatformAdapter {
|
|
3
4
|
getName() {
|
|
4
5
|
return 'BIGO Ads';
|
|
@@ -39,6 +40,7 @@ export class BigoAdapter extends PlatformAdapter {
|
|
|
39
40
|
const comment = `<!-- BIGO: HTML/ZIP format, config.json includes orientation when present, use BGY_MRAID.open() -->`;
|
|
40
41
|
// 在 </head> 之前插入
|
|
41
42
|
html = html.replace('</head>', `${sdkScript}${fallbackScript}${comment}</head>`);
|
|
43
|
+
html = injectBigoLifecycleBridge(html);
|
|
42
44
|
// 注入统一的 CTA 适配器
|
|
43
45
|
html = await this.injectCTAAdapterAsync(html);
|
|
44
46
|
html = await this.injectTrackingAdapterAsync(html);
|
package/dist/platforms/unity.js
CHANGED
|
@@ -1,4 +1,35 @@
|
|
|
1
1
|
import { PlatformAdapter } from './base.js';
|
|
2
|
+
const CHARSET_META_PATTERN = /<meta\b(?=[^>]*(?:\bcharset\s*=|\bhttp-equiv\s*=\s*(?:["']content-type["']|content-type)\b))[^>]*>/i;
|
|
3
|
+
function maskHeadRawText(content) {
|
|
4
|
+
return content.replace(/<!--[\s\S]*?(?:-->|$)|<(script|style|template|noscript)\b[^>]*>[\s\S]*?(?:<\/\1\s*>|$)/gi, (match) => match.replace(/[^\r\n]/g, ' '));
|
|
5
|
+
}
|
|
6
|
+
function injectUnityGate(html, gateMarkup) {
|
|
7
|
+
const headMatch = /<head(?:\s[^>]*)?>/i.exec(html);
|
|
8
|
+
if (!headMatch) {
|
|
9
|
+
// 无 head 的非标准文档不做节点重排:优先在首个业务脚本前注入。
|
|
10
|
+
const firstScript = /<script\b/i.exec(html);
|
|
11
|
+
if (firstScript) {
|
|
12
|
+
return `${html.slice(0, firstScript.index)}${gateMarkup}${html.slice(firstScript.index)}`;
|
|
13
|
+
}
|
|
14
|
+
const bodyClose = /<\/body>/i.exec(html);
|
|
15
|
+
if (bodyClose) {
|
|
16
|
+
return `${html.slice(0, bodyClose.index)}${gateMarkup}${html.slice(bodyClose.index)}`;
|
|
17
|
+
}
|
|
18
|
+
return `${html}${gateMarkup}`;
|
|
19
|
+
}
|
|
20
|
+
const headEnd = headMatch.index + headMatch[0].length;
|
|
21
|
+
const headCloseMatch = /<\/head>/i.exec(html.slice(headEnd));
|
|
22
|
+
const headContentEnd = headCloseMatch ? headEnd + headCloseMatch.index : html.length;
|
|
23
|
+
const headContent = html.slice(headEnd, headContentEnd);
|
|
24
|
+
const charsetMatch = CHARSET_META_PATTERN.exec(maskHeadRawText(headContent));
|
|
25
|
+
if (!charsetMatch) {
|
|
26
|
+
return `${html.slice(0, headEnd)}${gateMarkup}${html.slice(headEnd)}`;
|
|
27
|
+
}
|
|
28
|
+
// charset 必须位于文档开头区域;移动到 head 首位后再注入门控,确保门控仍早于业务脚本。
|
|
29
|
+
const charsetStart = headEnd + charsetMatch.index;
|
|
30
|
+
const htmlWithoutCharset = html.slice(0, charsetStart) + html.slice(charsetStart + charsetMatch[0].length);
|
|
31
|
+
return `${htmlWithoutCharset.slice(0, headEnd)}${charsetMatch[0]}${gateMarkup}${htmlWithoutCharset.slice(headEnd)}`;
|
|
32
|
+
}
|
|
2
33
|
export class UnityAdapter extends PlatformAdapter {
|
|
3
34
|
getName() {
|
|
4
35
|
return 'Unity Ads';
|
|
@@ -11,10 +42,19 @@ export class UnityAdapter extends PlatformAdapter {
|
|
|
11
42
|
return 'html';
|
|
12
43
|
}
|
|
13
44
|
async modifyHTML(html, assets) {
|
|
14
|
-
|
|
15
|
-
const
|
|
45
|
+
const unityScript = await this.minifyPlatformScript(this.getPlatformScript(), 'unity-mraid');
|
|
46
|
+
const comment = `<!-- Unity Ads: MRAID 2.0, 等待viewableChange, 使用mraid.open() -->`;
|
|
47
|
+
// 保留 charset 在文档开头,同时确保可见性门控早于首个业务脚本。
|
|
48
|
+
html = injectUnityGate(html, `${unityScript}${comment}`);
|
|
49
|
+
// 注入统一的 CTA 适配器
|
|
50
|
+
html = await this.injectCTAAdapterAsync(html);
|
|
51
|
+
html = await this.injectTrackingAdapterAsync(html);
|
|
52
|
+
return html;
|
|
53
|
+
}
|
|
54
|
+
getPlatformScript() {
|
|
55
|
+
return `
|
|
16
56
|
window.mraid = window.mraid || {
|
|
17
|
-
getVersion: function() { return '
|
|
57
|
+
getVersion: function() { return '2.0'; },
|
|
18
58
|
isReady: function() { return true; },
|
|
19
59
|
open: function(url) {
|
|
20
60
|
console.log('Unity Ads CTA: opening store');
|
|
@@ -25,8 +65,10 @@ export class UnityAdapter extends PlatformAdapter {
|
|
|
25
65
|
},
|
|
26
66
|
close: function() { window.close(); },
|
|
27
67
|
addEventListener: function(event, listener) {
|
|
28
|
-
if (event === 'ready'
|
|
29
|
-
setTimeout(function() { listener(
|
|
68
|
+
if (event === 'ready') {
|
|
69
|
+
setTimeout(function() { listener(); }, 0);
|
|
70
|
+
} else if (event === 'viewableChange') {
|
|
71
|
+
setTimeout(function() { listener(true); }, 0);
|
|
30
72
|
}
|
|
31
73
|
},
|
|
32
74
|
removeEventListener: function(event, listener) {},
|
|
@@ -37,29 +79,79 @@ export class UnityAdapter extends PlatformAdapter {
|
|
|
37
79
|
getCurrentPosition: function() { return { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight }; },
|
|
38
80
|
getScreenSize: function() { return { width: screen.width, height: screen.height }; }
|
|
39
81
|
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
|
|
83
|
+
(function() {
|
|
84
|
+
var nativeRequestAnimationFrame = window.requestAnimationFrame
|
|
85
|
+
? window.requestAnimationFrame.bind(window)
|
|
86
|
+
: function(callback) {
|
|
87
|
+
return window.setTimeout(function() { callback(Date.now()); }, 16);
|
|
88
|
+
};
|
|
89
|
+
var nativeCancelAnimationFrame = window.cancelAnimationFrame
|
|
90
|
+
? window.cancelAnimationFrame.bind(window)
|
|
91
|
+
: window.clearTimeout.bind(window);
|
|
92
|
+
var queuedFrames = [];
|
|
93
|
+
var nextQueuedFrameId = -1;
|
|
94
|
+
var viewable = false;
|
|
95
|
+
var previousVisibility = document.documentElement.style.visibility;
|
|
96
|
+
|
|
97
|
+
window.__PLAYCRAFT_UNITY_VIEWABLE__ = false;
|
|
98
|
+
document.documentElement.style.visibility = 'hidden';
|
|
99
|
+
|
|
100
|
+
window.requestAnimationFrame = function(callback) {
|
|
57
101
|
if (viewable) {
|
|
58
|
-
|
|
59
|
-
// 这里启动游戏内容
|
|
102
|
+
return nativeRequestAnimationFrame(callback);
|
|
60
103
|
}
|
|
61
|
-
|
|
62
|
-
|
|
104
|
+
var id = nextQueuedFrameId--;
|
|
105
|
+
queuedFrames.push({ id: id, callback: callback });
|
|
106
|
+
return id;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
window.cancelAnimationFrame = function(id) {
|
|
110
|
+
if (id < 0) {
|
|
111
|
+
queuedFrames = queuedFrames.filter(function(frame) { return frame.id !== id; });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
nativeCancelAnimationFrame(id);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function dispatchLifecycleEvent(name) {
|
|
118
|
+
var event = document.createEvent('Event');
|
|
119
|
+
event.initEvent(name, false, false);
|
|
120
|
+
window.dispatchEvent(event);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function setViewable(nextViewable) {
|
|
124
|
+
var normalizedViewable = nextViewable === true;
|
|
125
|
+
if (viewable === normalizedViewable) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
viewable = normalizedViewable;
|
|
129
|
+
window.__PLAYCRAFT_UNITY_VIEWABLE__ = viewable;
|
|
130
|
+
document.documentElement.style.visibility = viewable ? previousVisibility : 'hidden';
|
|
131
|
+
|
|
132
|
+
if (!viewable) {
|
|
133
|
+
dispatchLifecycleEvent('playcraft:unity-hidden');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
var pendingFrames = queuedFrames;
|
|
138
|
+
queuedFrames = [];
|
|
139
|
+
pendingFrames.forEach(function(frame) {
|
|
140
|
+
nativeRequestAnimationFrame(frame.callback);
|
|
141
|
+
});
|
|
142
|
+
dispatchLifecycleEvent('playcraft:unity-viewable');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function onSdkReady() {
|
|
146
|
+
mraid.addEventListener('viewableChange', setViewable);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof mraid.getState === 'function' && mraid.getState() === 'loading') {
|
|
150
|
+
mraid.addEventListener('ready', onSdkReady);
|
|
151
|
+
} else {
|
|
152
|
+
onSdkReady();
|
|
153
|
+
}
|
|
154
|
+
})();
|
|
63
155
|
`;
|
|
64
156
|
}
|
|
65
157
|
validateOptions() {
|
|
@@ -160,13 +160,12 @@ export const CHANNEL_RULE_PROFILES = {
|
|
|
160
160
|
], { optionalStages: ['delivery-package', 'runtime-manual'], deliveryPackageFormat: 'mixed' }),
|
|
161
161
|
tiktok: profile('tiktok', 'TikTok / Pangle', 'zip', [
|
|
162
162
|
forbiddenPatternRule('tiktok', 'sdk.mraid-js.forbidden', 'TikTok/Pangle creatives should not include mraid.js.', 'mraid\\.js', 'Remove mraid.js; Pangle integration belongs to the channel SDK.'),
|
|
163
|
-
forbiddenPatternRule('tiktok', 'redirect.javascript.forbidden', 'TikTok/Pangle creatives should not contain
|
|
163
|
+
forbiddenPatternRule('tiktok', 'redirect.javascript.forbidden', 'TikTok/Pangle creatives should not contain direct location redirects.', '\\blocation\\.(href|assign|replace)\\b', 'Remove direct location redirects. CTA handling is injected by @playcraft/devkit and delegated to Pangle.'),
|
|
164
164
|
], { optionalStages: ['delivery-package'], deliveryPackageFormat: 'zip', devkitOwned: true, internalBuildFormat: 'delegated' }),
|
|
165
165
|
unity: profile('unity', 'Unity Ads', 'single-html', [
|
|
166
|
-
sourcePresenceRule('unity', 'mraid.viewable-change.required', 'Unity requires MRAID viewableChange before gameplay starts.', 'Add a viewableChange listener before starting playable content.'),
|
|
167
166
|
externalResourceRule('unity'),
|
|
168
167
|
requiredPatternRule('unity', 'cta.mraid-open.required', 'Unity CTA should use mraid.open().', '\\bmraid\\.open\\s*\\(', 'Route the CTA directly to the store through mraid.open().'),
|
|
169
|
-
requiredPatternRule('unity', 'mraid.viewable-change.required', 'Unity playable should wait for MRAID viewableChange.', 'viewableChange'
|
|
168
|
+
requiredPatternRule('unity', 'mraid.viewable-change.required', 'Unity playable should wait for MRAID viewableChange.', `mraid\\.addEventListener\\s*\\(\\s*['"]viewableChange['"]`, 'Wait for the MRAID viewableChange event before starting gameplay.'),
|
|
170
169
|
manualRule('unity', 'runtime.orientation-and-start.manual', 'Orientation support and gameplay start timing need runtime review.', 'Verify portrait/landscape support and that gameplay waits for viewability.'),
|
|
171
170
|
], { optionalStages: ['runtime-manual'] }),
|
|
172
171
|
ironsource: profile('ironsource', 'ironSource', 'mixed', [
|
|
@@ -184,7 +183,7 @@ export const CHANNEL_RULE_PROFILES = {
|
|
|
184
183
|
manualRule('moloco', 'runtime.portrait-responsive.manual', 'Portrait responsive behavior needs runtime review.', 'Verify portrait layout and CTA visibility.'),
|
|
185
184
|
], { optionalStages: ['runtime-manual'] }),
|
|
186
185
|
bigo: profile('bigo', 'BIGO', 'zip', [
|
|
187
|
-
sourcePresenceRule('bigo', 'lifecycle.game-end.required', 'BIGO requires
|
|
186
|
+
sourcePresenceRule('bigo', 'lifecycle.game-end.required', 'BIGO requires a gameplay finish signal.', 'Call sdk.playcraftFinish() when gameplay ends; the BIGO channel build maps it to window.BGY_MRAID.gameEnd().'),
|
|
188
187
|
externalResourceRule('bigo', ['static-web.likeevideo.com']),
|
|
189
188
|
requiredPatternRule('bigo', 'cta.bgy-open.required', 'BIGO CTA should use window.BGY_MRAID.open().', 'window\\.BGY_MRAID\\.open\\s*\\(', 'Route CTA clicks through window.BGY_MRAID.open().'),
|
|
190
189
|
requiredPatternRule('bigo', 'lifecycle.game-end.required', 'BIGO creatives should call window.BGY_MRAID.gameEnd().', 'window\\.BGY_MRAID\\.gameEnd\\s*\\(', 'Call gameEnd() when gameplay ends.'),
|
|
@@ -203,8 +202,8 @@ export const CHANNEL_RULE_PROFILES = {
|
|
|
203
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.'),
|
|
204
203
|
]),
|
|
205
204
|
mintegral: profile('mintegral', 'Mintegral', 'mixed', [
|
|
206
|
-
sourcePresenceRule('mintegral', 'lifecycle.
|
|
207
|
-
requiredPatternRule('mintegral', 'lifecycle.
|
|
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
|
+
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().'),
|
|
208
207
|
forbiddenPatternRule('mintegral', 'redirect.auto.forbidden', 'Mintegral creatives should not auto-redirect to the store.', '\\blocation\\.(href|assign|replace)\\b', 'Keep store jumps behind a user CTA.'),
|
|
209
208
|
externalValidatorRule('mintegral', 'Mindworks testing should be used before delivery.', 'Validate through the Mintegral/Mindworks testing entry.'),
|
|
210
209
|
], { optionalStages: ['external-validator'], deliveryPackageFormat: 'mixed' }),
|
|
@@ -54,22 +54,12 @@ const COMMON_SOURCE_PATTERNS = [
|
|
|
54
54
|
},
|
|
55
55
|
];
|
|
56
56
|
const CHANNEL_SOURCE_PATTERNS = {
|
|
57
|
-
unity: [
|
|
58
|
-
{
|
|
59
|
-
ruleId: 'unity.mraid.viewable-change.required',
|
|
60
|
-
pattern: /viewableChange/g,
|
|
61
|
-
message: 'Unity requires waiting for MRAID viewableChange before starting playable content.',
|
|
62
|
-
recommendation: 'Add a viewableChange listener and start gameplay only after the ad is viewable.',
|
|
63
|
-
severity: 'error',
|
|
64
|
-
confidence: 'high',
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
57
|
mintegral: [
|
|
68
58
|
{
|
|
69
|
-
ruleId: 'mintegral.lifecycle.
|
|
70
|
-
pattern:
|
|
71
|
-
message: 'Mintegral requires
|
|
72
|
-
recommendation: 'Call sdk.
|
|
59
|
+
ruleId: 'mintegral.lifecycle.game-end.required',
|
|
60
|
+
pattern: /(?:\b[\w$]+(?:\?\.|\.)playcraftFinish(?:\?\.)?|window(?:\?\.|\.)gameEnd(?:\?\.)?)\s*\(/g,
|
|
61
|
+
message: 'Mintegral requires a gameplay finish signal.',
|
|
62
|
+
recommendation: 'Call sdk.playcraftFinish() when the win or end state is reached; the channel build maps it to window.gameEnd().',
|
|
73
63
|
severity: 'error',
|
|
74
64
|
confidence: 'high',
|
|
75
65
|
},
|
|
@@ -77,9 +67,9 @@ const CHANNEL_SOURCE_PATTERNS = {
|
|
|
77
67
|
bigo: [
|
|
78
68
|
{
|
|
79
69
|
ruleId: 'bigo.lifecycle.game-end.required',
|
|
80
|
-
pattern: /window
|
|
81
|
-
message: 'BIGO requires
|
|
82
|
-
recommendation: 'Call
|
|
70
|
+
pattern: /(?:\b[\w$]+(?:\?\.|\.)playcraftFinish(?:\?\.)?|window(?:\?\.|\.)BGY_MRAID(?:\?\.|\.)gameEnd(?:\?\.)?)\s*\(/g,
|
|
71
|
+
message: 'BIGO requires a gameplay finish signal.',
|
|
72
|
+
recommendation: 'Call sdk.playcraftFinish() when gameplay ends; the BIGO channel build maps it to window.BGY_MRAID.gameEnd().',
|
|
83
73
|
severity: 'error',
|
|
84
74
|
confidence: 'high',
|
|
85
75
|
},
|
|
@@ -130,7 +120,7 @@ async function scanSourceFilesForChannel(context, sourceFiles, includeCommonPatt
|
|
|
130
120
|
if (matchedRequiredRuleIds.has(pattern.ruleId)) {
|
|
131
121
|
continue;
|
|
132
122
|
}
|
|
133
|
-
const match = content
|
|
123
|
+
const match = findExecutableSourcePattern(filePath, content, pattern.pattern);
|
|
134
124
|
if (match) {
|
|
135
125
|
matchedRequiredRuleIds.add(pattern.ruleId);
|
|
136
126
|
results.push({
|
|
@@ -142,8 +132,8 @@ async function scanSourceFilesForChannel(context, sourceFiles, includeCommonPatt
|
|
|
142
132
|
message: `${pattern.message}: passed`,
|
|
143
133
|
recommendation: pattern.recommendation,
|
|
144
134
|
file: filePath,
|
|
145
|
-
line: lineOfIndex(content,
|
|
146
|
-
evidence: trimEvidence(match
|
|
135
|
+
line: lineOfIndex(content, match.index),
|
|
136
|
+
evidence: trimEvidence(match.evidence),
|
|
147
137
|
owner: ownerForContext(context),
|
|
148
138
|
confidence: pattern.confidence,
|
|
149
139
|
});
|
|
@@ -153,6 +143,278 @@ async function scanSourceFilesForChannel(context, sourceFiles, includeCommonPatt
|
|
|
153
143
|
results.push(...missingRequiredPatternResults(context, matchedRequiredRuleIds));
|
|
154
144
|
return results;
|
|
155
145
|
}
|
|
146
|
+
function findExecutableSourcePattern(filePath, content, pattern) {
|
|
147
|
+
const executableSource = maskNonExecutableSource(filePath, content);
|
|
148
|
+
const expression = new RegExp(pattern.source, pattern.flags);
|
|
149
|
+
expression.lastIndex = 0;
|
|
150
|
+
const match = expression.exec(executableSource);
|
|
151
|
+
if (!match || match.index == null) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
index: match.index,
|
|
156
|
+
evidence: content.slice(match.index, match.index + match[0].length),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function maskNonExecutableSource(filePath, content) {
|
|
160
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
161
|
+
if (extension === '.css') {
|
|
162
|
+
return blankSource(content);
|
|
163
|
+
}
|
|
164
|
+
if (extension === '.html') {
|
|
165
|
+
return maskHtmlSource(content);
|
|
166
|
+
}
|
|
167
|
+
return maskJavaScriptLikeSource(content);
|
|
168
|
+
}
|
|
169
|
+
function maskHtmlSource(content) {
|
|
170
|
+
const masked = blankSource(content).split('');
|
|
171
|
+
const lowerContent = content.toLowerCase();
|
|
172
|
+
let cursor = 0;
|
|
173
|
+
while (cursor < content.length) {
|
|
174
|
+
const commentStart = lowerContent.indexOf('<!--', cursor);
|
|
175
|
+
const scriptStart = lowerContent.indexOf('<script', cursor);
|
|
176
|
+
if (scriptStart < 0) {
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
if (commentStart >= 0 && commentStart < scriptStart) {
|
|
180
|
+
const commentEnd = lowerContent.indexOf('-->', commentStart + 4);
|
|
181
|
+
cursor = commentEnd < 0 ? content.length : commentEnd + 3;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const tagEnd = findHtmlTagEnd(content, scriptStart + 7);
|
|
185
|
+
if (tagEnd < 0) {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
const openTag = content.slice(scriptStart, tagEnd + 1);
|
|
189
|
+
const closeTagStart = lowerContent.indexOf('</script', tagEnd + 1);
|
|
190
|
+
const scriptEnd = closeTagStart < 0 ? content.length : closeTagStart;
|
|
191
|
+
if (isExecutableScriptTag(openTag)) {
|
|
192
|
+
const scriptSource = content.slice(tagEnd + 1, scriptEnd);
|
|
193
|
+
const maskedScript = maskJavaScriptLikeSource(scriptSource);
|
|
194
|
+
for (let index = 0; index < maskedScript.length; index += 1) {
|
|
195
|
+
masked[tagEnd + 1 + index] = maskedScript[index];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (closeTagStart < 0) {
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
const closeTagEnd = findHtmlTagEnd(content, closeTagStart + 8);
|
|
202
|
+
cursor = closeTagEnd < 0 ? content.length : closeTagEnd + 1;
|
|
203
|
+
}
|
|
204
|
+
return masked.join('');
|
|
205
|
+
}
|
|
206
|
+
function findHtmlTagEnd(content, start) {
|
|
207
|
+
let quote = null;
|
|
208
|
+
for (let index = start; index < content.length; index += 1) {
|
|
209
|
+
const character = content[index];
|
|
210
|
+
if (quote) {
|
|
211
|
+
if (character === quote) {
|
|
212
|
+
quote = null;
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (character === '"' || character === "'") {
|
|
217
|
+
quote = character;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (character === '>') {
|
|
221
|
+
return index;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return -1;
|
|
225
|
+
}
|
|
226
|
+
function isExecutableScriptTag(openTag) {
|
|
227
|
+
const typeMatch = openTag.match(/\btype\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i);
|
|
228
|
+
if (!typeMatch) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
const type = (typeMatch[1] ?? typeMatch[2] ?? typeMatch[3] ?? '').trim().toLowerCase();
|
|
232
|
+
if (!type) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
const mimeType = type.split(';', 1)[0].trim();
|
|
236
|
+
return mimeType === 'module' || /^(?:text|application)\/(?:java|ecma)script$/.test(mimeType);
|
|
237
|
+
}
|
|
238
|
+
function maskJavaScriptLikeSource(content) {
|
|
239
|
+
const masked = content.split('');
|
|
240
|
+
const maskAt = (index) => {
|
|
241
|
+
if (content[index] !== '\n' && content[index] !== '\r') {
|
|
242
|
+
masked[index] = ' ';
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const maskRange = (start, end) => {
|
|
246
|
+
for (let index = start; index < end; index += 1) {
|
|
247
|
+
maskAt(index);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const scanQuoted = (start, quote) => {
|
|
251
|
+
let index = start + 1;
|
|
252
|
+
while (index < content.length) {
|
|
253
|
+
if (content[index] === '\\') {
|
|
254
|
+
index = Math.min(content.length, index + 2);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (content[index] === quote) {
|
|
258
|
+
index += 1;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
index += 1;
|
|
262
|
+
}
|
|
263
|
+
maskRange(start, index);
|
|
264
|
+
return index;
|
|
265
|
+
};
|
|
266
|
+
const scanLineComment = (start) => {
|
|
267
|
+
let index = start + 2;
|
|
268
|
+
while (index < content.length && content[index] !== '\n' && content[index] !== '\r') {
|
|
269
|
+
index += 1;
|
|
270
|
+
}
|
|
271
|
+
maskRange(start, index);
|
|
272
|
+
return index;
|
|
273
|
+
};
|
|
274
|
+
const scanBlockComment = (start, closingToken) => {
|
|
275
|
+
const closingIndex = content.indexOf(closingToken, start + 2);
|
|
276
|
+
const end = closingIndex < 0 ? content.length : closingIndex + closingToken.length;
|
|
277
|
+
maskRange(start, end);
|
|
278
|
+
return end;
|
|
279
|
+
};
|
|
280
|
+
const previousSignificantIndex = (start) => {
|
|
281
|
+
for (let index = start; index >= 0; index -= 1) {
|
|
282
|
+
if (!/\s/.test(masked[index])) {
|
|
283
|
+
return index;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return -1;
|
|
287
|
+
};
|
|
288
|
+
const isRegexStart = (index) => {
|
|
289
|
+
if (content[index + 1] === '=') {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
const previousIndex = previousSignificantIndex(index - 1);
|
|
293
|
+
if (previousIndex < 0) {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
if (/[([{,:;=!?&|+\-*%^~<>}]/.test(masked[previousIndex])) {
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
let wordStart = previousIndex;
|
|
300
|
+
while (wordStart >= 0 && /[\w$]/.test(masked[wordStart])) {
|
|
301
|
+
wordStart -= 1;
|
|
302
|
+
}
|
|
303
|
+
const previousWord = masked.slice(wordStart + 1, previousIndex + 1).join('');
|
|
304
|
+
return /^(?:await|case|delete|do|else|in|instanceof|new|of|return|throw|typeof|void|yield)$/.test(previousWord);
|
|
305
|
+
};
|
|
306
|
+
const scanRegex = (start) => {
|
|
307
|
+
let index = start + 1;
|
|
308
|
+
let inCharacterClass = false;
|
|
309
|
+
while (index < content.length) {
|
|
310
|
+
const character = content[index];
|
|
311
|
+
if (character === '\n' || character === '\r') {
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
if (character === '\\') {
|
|
315
|
+
index = Math.min(content.length, index + 2);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (character === '[') {
|
|
319
|
+
inCharacterClass = true;
|
|
320
|
+
}
|
|
321
|
+
else if (character === ']') {
|
|
322
|
+
inCharacterClass = false;
|
|
323
|
+
}
|
|
324
|
+
else if (character === '/' && !inCharacterClass) {
|
|
325
|
+
index += 1;
|
|
326
|
+
while (index < content.length && /[a-z]/i.test(content[index])) {
|
|
327
|
+
index += 1;
|
|
328
|
+
}
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
index += 1;
|
|
332
|
+
}
|
|
333
|
+
maskRange(start, index);
|
|
334
|
+
return index;
|
|
335
|
+
};
|
|
336
|
+
let scanCode;
|
|
337
|
+
const scanTemplate = (start) => {
|
|
338
|
+
maskAt(start);
|
|
339
|
+
let index = start + 1;
|
|
340
|
+
while (index < content.length) {
|
|
341
|
+
if (content[index] === '\\') {
|
|
342
|
+
maskAt(index);
|
|
343
|
+
if (index + 1 < content.length) {
|
|
344
|
+
maskAt(index + 1);
|
|
345
|
+
}
|
|
346
|
+
index += 2;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (content[index] === '`') {
|
|
350
|
+
maskAt(index);
|
|
351
|
+
return index + 1;
|
|
352
|
+
}
|
|
353
|
+
if (content[index] === '$' && content[index + 1] === '{') {
|
|
354
|
+
maskAt(index);
|
|
355
|
+
maskAt(index + 1);
|
|
356
|
+
index = scanCode(index + 2, true);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
maskAt(index);
|
|
360
|
+
index += 1;
|
|
361
|
+
}
|
|
362
|
+
return index;
|
|
363
|
+
};
|
|
364
|
+
scanCode = (start, interpolation) => {
|
|
365
|
+
let index = start;
|
|
366
|
+
let braceDepth = interpolation ? 1 : 0;
|
|
367
|
+
while (index < content.length) {
|
|
368
|
+
const character = content[index];
|
|
369
|
+
const nextCharacter = content[index + 1];
|
|
370
|
+
if (interpolation && character === '{') {
|
|
371
|
+
braceDepth += 1;
|
|
372
|
+
index += 1;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (interpolation && character === '}') {
|
|
376
|
+
braceDepth -= 1;
|
|
377
|
+
if (braceDepth === 0) {
|
|
378
|
+
maskAt(index);
|
|
379
|
+
return index + 1;
|
|
380
|
+
}
|
|
381
|
+
index += 1;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (character === '"' || character === "'") {
|
|
385
|
+
index = scanQuoted(index, character);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (character === '`') {
|
|
389
|
+
index = scanTemplate(index);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (character === '/' && nextCharacter === '/') {
|
|
393
|
+
index = scanLineComment(index);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (character === '/' && nextCharacter === '*') {
|
|
397
|
+
index = scanBlockComment(index, '*/');
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (character === '<' && content.startsWith('<!--', index)) {
|
|
401
|
+
index = scanBlockComment(index, '-->');
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (character === '/' && isRegexStart(index)) {
|
|
405
|
+
index = scanRegex(index);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
index += 1;
|
|
409
|
+
}
|
|
410
|
+
return index;
|
|
411
|
+
};
|
|
412
|
+
scanCode(0, false);
|
|
413
|
+
return masked.join('');
|
|
414
|
+
}
|
|
415
|
+
function blankSource(content) {
|
|
416
|
+
return content.replace(/[^\r\n]/g, ' ');
|
|
417
|
+
}
|
|
156
418
|
function evaluateSourcePatternForChannel(context, channel, filePath, content, sourcePattern) {
|
|
157
419
|
const matches = Array.from(content.matchAll(sourcePattern.pattern));
|
|
158
420
|
return matches.slice(0, 10).map((match) => ({
|
|
@@ -218,7 +218,7 @@ export const PLATFORM_CONFIGS = {
|
|
|
218
218
|
minifyJS: true,
|
|
219
219
|
compressImages: false, // 禁用图片压缩,原图输出效果更好
|
|
220
220
|
compressModels: true,
|
|
221
|
-
injectScripts: ['
|
|
221
|
+
injectScripts: ['mraid'],
|
|
222
222
|
outputFileName: 'index.html',
|
|
223
223
|
includeSourcemap: false,
|
|
224
224
|
imageQuality: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/build",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.50",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"@vheemstra/vite-plugin-imagemin": "^2.2.1",
|
|
43
43
|
"archiver": "^7.0.1",
|
|
44
44
|
"draco3dgltf": "^1.5.7",
|
|
45
|
+
"fflate": "^0.8.2",
|
|
45
46
|
"imagemin-mozjpeg": "^10.0.0",
|
|
46
47
|
"imagemin-pngquant": "^10.0.0",
|
|
47
48
|
"imagemin-webp": "^8.0.0",
|
|
@@ -59,7 +60,6 @@
|
|
|
59
60
|
"@types/archiver": "^6.0.4",
|
|
60
61
|
"@types/mime-types": "^2.1.4",
|
|
61
62
|
"@types/node": "^22.19.8",
|
|
62
|
-
"fflate": "^0.8.2",
|
|
63
63
|
"typescript": "^5.9.3"
|
|
64
64
|
}
|
|
65
65
|
}
|