update playworks checker

This commit is contained in:
2026-05-28 14:17:26 +08:00
parent 4f17523f26
commit cce30c0729
20 changed files with 616 additions and 54 deletions

View File

@@ -95,8 +95,8 @@ export function openPlayworksConverter(_context: vscode.ExtensionContext) {
}
const ZIPPED_NETWORKS = new Set(['gg', 'vu', 'mtg']);
const SUPPORTED_NETWORKS = new Set(['al', 'is', 'fb', 'gg', 'mo', 'vu', 'mtg', 'tt']);
const MRAID_NETWORKS = new Set(['al', 'is']);
const SUPPORTED_NETWORKS = new Set(['al', 'is', 'fb', 'gg', 'mo', 'vu', 'mtg', 'un', 'tt']);
const MRAID_NETWORKS = new Set(['al', 'is', 'un']);
const NETWORK_OUTPUT_FOLDERS: Record<string, string> = {
al: 'Applovin',
is: 'Ironsource',
@@ -105,6 +105,7 @@ const NETWORK_OUTPUT_FOLDERS: Record<string, string> = {
mo: 'Moloco',
vu: 'Vungle',
mtg: 'Mintegral',
un: 'Unity',
tt: 'TikTok',
};
@@ -149,15 +150,15 @@ async function convertNetwork(html: string, opts: ConvertOptions, network: strin
}
function transformHtml(html: string, network: string): string {
let result = html;
let result = sanitizePlayworksHtml(html);
// Inject into </head>: lifecycle stubs only (must register luna:build listener
// BEFORE the Playworks bundle so our handler fires first), then any network SDK script.
// Console restore stays at end-of-body — it must run AFTER the bundle overrides console.
result = removeNetworkSdkScripts(result, network);
result = removeNetworkSdkScripts(result);
let headInject = buildLifecycleScript();
if (MRAID_NETWORKS.has(network)) {
headInject += '<script src="mraid.js"></script>';
headInject += '<script src="mraid.js"></script>' + buildMraidComplianceScript();
} else if (network === 'gg') {
headInject += '<script src="exitapi.js"></script>';
}
@@ -175,16 +176,60 @@ function transformHtml(html: string, network: string): string {
// Replace the CTA script
result = replaceCTAScript(result, network);
return finalizeNetworkHtml(result, network);
}
function sanitizePlayworksHtml(html: string): string {
let result = cleanupPlayworksHtml(html);
return removeNetworkSdkScripts(result);
}
function cleanupPlayworksHtml(html: string): string {
let result = html;
result = trimAfterFirstHtmlClose(result);
result = result.replace(/<script\b[^>]*>[\s\S]*?insertYourRemoteDebuggingTokenHere[\s\S]*?<\/script>\s*/gi, '');
result = result.replace(/https:\/\/mrdoob\.github\.io\/stats\.js\/build\/stats\.min\.js/gi, 'data:text/javascript,');
result = result.replace(/\s+crossorigin(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?/gi, '');
result = result.replace(/\s+type\s*=\s*["']module["']/gi, '');
result = result.replace(/\bconsole\.error\s*\(/g, 'console.warn(');
return result;
}
function removeNetworkSdkScripts(html: string, network: string): string {
function trimAfterFirstHtmlClose(html: string): string {
const idx = html.search(/<\/html\s*>/i);
if (idx === -1) return html;
const close = html.match(/<\/html\s*>/i);
return close ? html.slice(0, idx + close[0].length) : html;
}
function finalizeNetworkHtml(html: string, network: string): string {
let result = cleanupPlayworksHtml(html);
result = dedupeScriptSrc(result, 'mraid.js', MRAID_NETWORKS.has(network));
result = dedupeScriptSrc(result, 'exitapi.js', network === 'gg');
return result;
}
function removeNetworkSdkScripts(html: string): string {
let result = html;
if (MRAID_NETWORKS.has(network)) {
result = result.replace(/<script\b[^>]*\bsrc\s*=\s*["']mraid\.js["'][^>]*>\s*<\/script>\s*/gi, '');
for (const src of ['mraid.js', 'exitapi.js']) {
const escaped = src.replace('.', '\\.');
result = result.replace(new RegExp(`<script\\b[^>]*\\bsrc\\s*=\\s*["']${escaped}["'][^>]*>\\s*<\\/script>\\s*`, 'gi'), '');
}
if (network === 'gg') {
result = result.replace(/<script\b[^>]*\bsrc\s*=\s*["']exitapi\.js["'][^>]*>\s*<\/script>\s*/gi, '');
return result;
}
function dedupeScriptSrc(html: string, src: string, shouldExist: boolean): string {
const escaped = src.replace('.', '\\.');
let seen = false;
let result = html.replace(new RegExp(`<script\\b[^>]*\\bsrc\\s*=\\s*["']${escaped}["'][^>]*>\\s*<\\/script>\\s*`, 'gi'), (match) => {
if (shouldExist && !seen) {
seen = true;
return `<script src="${src}"></script>`;
}
return '';
});
if (shouldExist && !seen) {
result = injectBeforeHeadClose(result, `<script src="${src}"></script>`);
}
return result;
}
@@ -273,6 +318,28 @@ function buildLifecycleScript(): string {
].join('');
}
function buildMraidComplianceScript(): string {
return [
'<script>(function(){',
'var m=null,scene=null,viewable=true,exposed=true,volume=null;',
'function get(){return window.mraid||m;}',
'function emit(name,detail){try{window.dispatchEvent(new CustomEvent(name,{detail:detail}));}catch(e){}}',
'function apply(){emit("hpl:mraid:visibility",{viewable:viewable,exposed:exposed,hidden:!viewable||!exposed});if(scene&&scene.sound&&typeof scene.sound.setMute==="function")scene.sound.setMute(!viewable||!exposed);}',
'function onViewable(v){viewable=!!v;apply();}',
'function onExposure(p){if(typeof p==="number")exposed=p>0;apply();}',
'function onVolume(v){if(typeof v==="number"){volume=v/100;emit("hpl:mraid:volume",volume);if(scene&&scene.sound&&typeof scene.sound.setVolume==="function")scene.sound.setVolume(volume);}}',
'function add(name,fn){var x=get();try{if(x&&typeof x.addEventListener==="function")x.addEventListener(name,fn);}catch(e){}}',
'function unload(){try{if(typeof mraid!=="undefined"&&typeof mraid.unload==="function")mraid.unload();}catch(e){}}',
'window.__hplMraidUnload=unload;',
'function setup(){var x=get();if(!x||setup.done)return;setup.done=true;m=x;try{if(typeof mraid!=="undefined"&&typeof mraid.isViewable==="function")viewable=!!mraid.isViewable();else if(typeof x.isViewable==="function")viewable=!!x.isViewable();}catch(e){}try{if(typeof mraid!=="undefined"&&typeof mraid.addEventListener==="function"){mraid.addEventListener("error",function(message,action){console.warn("[MRAID error]",{message:message,action:action});});mraid.addEventListener("stateChange",function(state){console.log("[MRAID stateChange]",state);});mraid.addEventListener("exposureChange",onExposure);mraid.addEventListener("viewableChange",onViewable);mraid.addEventListener("audioVolumeChange",onVolume);apply();return;}}catch(e){}add("error",function(message,action){console.warn("[MRAID error]",{message:message,action:action});});add("stateChange",function(state){console.log("[MRAID stateChange]",state);});add("exposureChange",onExposure);add("viewableChange",onViewable);add("audioVolumeChange",onVolume);apply();}',
'function ready(){var x=get();if(!x)return false;try{if(typeof mraid!=="undefined"&&typeof mraid.getState==="function"&&mraid.getState()==="loading"){mraid.addEventListener("ready",setup);return true;}if(typeof x.getState==="function"&&x.getState()==="loading"){add("ready",setup);return true;}}catch(e){}setup();return true;}',
'window.__hplMraidBindScene=function(s){scene=s;apply();if(typeof volume==="number"&&scene&&scene.sound&&typeof scene.sound.setVolume==="function")scene.sound.setVolume(volume);};',
'if(!ready()){var end=Date.now()+500;(function poll(){if(ready()||Date.now()>end)return;setTimeout(poll,50);})();}',
'setTimeout(setup,2000);',
'})();</script>',
].join('');
}
function buildCTAScript(network: string): string {
const urlSetup = `n=n||window.$environment.packageConfig.iosLink,i=i||window.$environment.packageConfig.androidLink;var o=/iphone|ipad|ipod|macintosh/i.test(window.navigator.userAgent.toLowerCase())?n:i;`;
// gameClose must fire before every redirect
@@ -282,6 +349,7 @@ function buildCTAScript(network: string): string {
switch (network) {
case 'al':
case 'is':
case 'un':
// MRAID — mraid.js injected in <head>
ctaLogic = `${closeCall}if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){var s=typeof mraid.getState==="function"?mraid.getState():"default";if(s!=="loading"){mraid.open(o);return;}}window.open(o,"_blank");`;
break;
@@ -353,6 +421,7 @@ function getHtml(): string {
{ tag: 'mo', label: 'Moloco', note: 'HTML + FbPlayableAd CTA' },
{ tag: 'vu', label: 'Vungle', note: 'ZIP + __VUNGLE__ flag' },
{ tag: 'mtg', label: 'Mintegral', note: 'ZIP + onload="gameReady()"' },
{ tag: 'un', label: 'Unity', note: 'HTML + mraid.js in head' },
{ tag: 'tt', label: 'TikTok', note: 'HTML + __TIKTOK__ flag' },
];