mraid checker
This commit is contained in:
@@ -32,6 +32,7 @@ interface MraidRule {
|
||||
}
|
||||
|
||||
interface ScanContext {
|
||||
filePath: string;
|
||||
html: string;
|
||||
scriptText: string;
|
||||
lowerHtml: string;
|
||||
@@ -118,7 +119,7 @@ export function openMraidChecker(_context: vscode.ExtensionContext) {
|
||||
for (const file of targets) {
|
||||
try {
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
const issues = checkMraid(content);
|
||||
const issues = checkMraid(content, file);
|
||||
const display = baseDir ? path.relative(baseDir, file) || path.basename(file) : file;
|
||||
results.push({ file: display, ok: issues.length === 0, issues });
|
||||
} catch {
|
||||
@@ -164,9 +165,10 @@ async function collectHtmlFiles(root: string): Promise<string[]> {
|
||||
return out;
|
||||
}
|
||||
|
||||
function checkMraid(html: string): MraidIssue[] {
|
||||
function checkMraid(html: string, filePath = ''): MraidIssue[] {
|
||||
const scriptText = extractScriptText(html);
|
||||
const ctx: ScanContext = {
|
||||
filePath,
|
||||
html,
|
||||
scriptText,
|
||||
lowerHtml: html.toLowerCase(),
|
||||
@@ -226,6 +228,14 @@ function hasMraidReference(ctx: ScanContext): boolean {
|
||||
return /\bmraid\b/i.test(ctx.html);
|
||||
}
|
||||
|
||||
function countMraidJsReferences(ctx: ScanContext): number {
|
||||
return (ctx.html.match(/\bmraid\.js\b/gi) || []).length;
|
||||
}
|
||||
|
||||
function hasViewportMeta(ctx: ScanContext): boolean {
|
||||
return /<meta\b[^>]*\bname\s*=\s*['"]viewport['"][^>]*>/i.test(ctx.html);
|
||||
}
|
||||
|
||||
function hasMraidCall(ctx: ScanContext, method: string): boolean {
|
||||
return ctx.mraidCallLines.has(method);
|
||||
}
|
||||
@@ -253,6 +263,77 @@ function hasReadyGate(ctx: ScanContext): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function hasDomReadyGate(ctx: ScanContext): boolean {
|
||||
return (
|
||||
/document\s*\.\s*readyState/i.test(ctx.scriptText) ||
|
||||
/DOMContentLoaded/i.test(ctx.scriptText) ||
|
||||
/\bwindow\s*\.\s*(addEventListener\s*\(\s*['"]load['"]|onload\b)/i.test(ctx.scriptText)
|
||||
);
|
||||
}
|
||||
|
||||
function hasReadyListenerAndStateFallback(ctx: ScanContext): boolean {
|
||||
return hasEventListener(ctx, 'ready') && hasMraidCall(ctx, 'getState');
|
||||
}
|
||||
|
||||
function hasMraidTypeofGuard(ctx: ScanContext): boolean {
|
||||
return /typeof\s+(?:window\s*\.\s*)?mraid\s*!={1,2}\s*['"]undefined['"]/i.test(ctx.scriptText);
|
||||
}
|
||||
|
||||
function hasMraidFunctionGuard(ctx: ScanContext, method: string): boolean {
|
||||
const escaped = method.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return new RegExp(`typeof\\s+(?:window\\s*\\.\\s*)?mraid\\s*\\.\\s*${escaped}\\s*={2,3}\\s*['"]function['"]|typeof\\s+(?:window\\s*\\.\\s*)?mraid\\s*\\.\\s*${escaped}\\s*!={1,2}\\s*['"]undefined['"]`, 'i').test(ctx.scriptText);
|
||||
}
|
||||
|
||||
function hasMraidReadyTimeout(ctx: ScanContext): boolean {
|
||||
return /setTimeout\s*\(/i.test(ctx.scriptText) && /\bmraid\b/i.test(ctx.scriptText);
|
||||
}
|
||||
|
||||
function hasLateMraidDetection(ctx: ScanContext): boolean {
|
||||
return /setInterval\s*\(|setTimeout\s*\(|requestAnimationFrame\s*\(/i.test(ctx.scriptText) && /window\s*\.\s*mraid|\bmraid\b/i.test(ctx.scriptText);
|
||||
}
|
||||
|
||||
function hasMraidOpenGuard(ctx: ScanContext): boolean {
|
||||
return (
|
||||
/typeof\s+(?:window\s*\.\s*)?mraid\s*\.\s*open\s*={2,3}\s*['"]function['"]/i.test(ctx.scriptText) ||
|
||||
/typeof\s+(?:window\s*\.\s*)?mraid\s*\.\s*open\s*!={1,2}\s*['"]undefined['"]/i.test(ctx.scriptText)
|
||||
);
|
||||
}
|
||||
|
||||
function hasMraidOpenLoadingGuard(ctx: ScanContext): boolean {
|
||||
return (
|
||||
/mraid\s*\.\s*getState\s*\(\s*\)\s*!={1,2}\s*['"]loading['"]/i.test(ctx.scriptText) ||
|
||||
/['"]loading['"]\s*!={1,2}\s*mraid\s*\.\s*getState\s*\(\s*\)/i.test(ctx.scriptText) ||
|
||||
/typeof\s+(?:window\s*\.\s*)?mraid\s*\.\s*getState\s*!={1,2}\s*['"]function['"]/i.test(ctx.scriptText) ||
|
||||
(hasMraidCall(ctx, 'getState') && /\b\w+\s*!={1,2}\s*['"]loading['"]|['"]loading['"]\s*!={1,2}\s*\w+\b/i.test(ctx.scriptText)) ||
|
||||
(hasMraidCall(ctx, 'getState') && /\b\w+\s*={2,3}\s*['"]loading['"][\s\S]{0,500}\bwindow\s*\.\s*open\s*\(/i.test(ctx.scriptText))
|
||||
);
|
||||
}
|
||||
|
||||
function hasAudioVolumeNullSafeHandling(ctx: ScanContext): boolean {
|
||||
return hasEventListener(ctx, 'audioVolumeChange') && (/typeof\s+\w+\s*={2,3}\s*['"]number['"]/i.test(ctx.scriptText) || /!=\s*null|!==\s*null/i.test(ctx.scriptText));
|
||||
}
|
||||
|
||||
function hasLifecycleSignal(ctx: ScanContext, name: string): boolean {
|
||||
return new RegExp(`\\b${name}\\b`, 'i').test(ctx.html);
|
||||
}
|
||||
|
||||
function hasUnsupportedHyperlink(ctx: ScanContext): boolean {
|
||||
return /<a\b[^>]*\bhref\s*=/i.test(ctx.html);
|
||||
}
|
||||
|
||||
function hasTwoPartExpand(ctx: ScanContext): boolean {
|
||||
return /mraid\s*\.\s*expand\s*\(\s*[^)\s]/i.test(ctx.scriptText);
|
||||
}
|
||||
|
||||
function opensVideoUrl(ctx: ScanContext): boolean {
|
||||
return /mraid\s*\.\s*open\s*\(\s*[^)]*\.(mp4|m4v|mov|webm|avi|m3u8)(?:['"`?#)]|$)/i.test(ctx.scriptText);
|
||||
}
|
||||
|
||||
function isInMraidNetworkPath(ctx: ScanContext): boolean {
|
||||
if (!ctx.filePath) return true;
|
||||
return isInSupportedMraidFolderPath(ctx.filePath) || /_(al|is|un)\.html?$/i.test(path.basename(ctx.filePath));
|
||||
}
|
||||
|
||||
function hasEventListener(ctx: ScanContext, eventName: string): boolean {
|
||||
const pattern = new RegExp(`mraid\\s*\\.\\s*addEventListener\\s*\\(\\s*['"]${eventName}['"]`, 'i');
|
||||
return pattern.test(ctx.scriptText);
|
||||
@@ -264,15 +345,32 @@ function hasSupportCheck(ctx: ScanContext, feature: string): boolean {
|
||||
}
|
||||
|
||||
function usesDirectBrowserNavigation(ctx: ScanContext): boolean {
|
||||
return (
|
||||
/\bwindow\s*\.\s*open\s*\(/i.test(ctx.scriptText) ||
|
||||
/\blocation\s*\.\s*(href|assign|replace)\b/i.test(ctx.scriptText)
|
||||
);
|
||||
return /\bwindow\s*\.\s*open\s*\(/i.test(ctx.scriptText);
|
||||
}
|
||||
|
||||
const MRAID_REFERENCE = 'MRAID 3.0 specification and MRAID 3.0 Best Practices Guide in mraidDocuments/';
|
||||
function usesLocationNavigation(ctx: ScanContext): boolean {
|
||||
return /\blocation\s*\.\s*(href|assign|replace)\b/i.test(ctx.scriptText);
|
||||
}
|
||||
|
||||
const MRAID_REFERENCE = 'GUIDE.md, MRAID 3.0 specification, and MRAID 3.0 Best Practices Guide.';
|
||||
|
||||
const MRAID_RULES: MraidRule[] = [
|
||||
{
|
||||
id: 'mraid-js-missing',
|
||||
severity: 'requirement',
|
||||
title: 'mraid.js reference not found',
|
||||
detail: 'MRAID creatives for AppLovin, ironSource, and Unity must request mraid.js early in the generated HTML.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => isInMraidNetworkPath(ctx) && countMraidJsReferences(ctx) === 0 ? issue(MRAID_RULES_BY_ID['mraid-js-missing']) : null,
|
||||
},
|
||||
{
|
||||
id: 'mraid-js-duplicate',
|
||||
severity: 'requirement',
|
||||
title: 'Multiple mraid.js references found',
|
||||
detail: 'Request mraid.js exactly once; duplicate references can inject multiple MRAID libraries and slow or destabilize the ad.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => countMraidJsReferences(ctx) > 1 ? issue(MRAID_RULES_BY_ID['mraid-js-duplicate']) : null,
|
||||
},
|
||||
{
|
||||
id: 'mraid-reference',
|
||||
severity: 'requirement',
|
||||
@@ -281,6 +379,30 @@ const MRAID_RULES: MraidRule[] = [
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => hasMraidReference(ctx) ? null : issue(MRAID_RULES_BY_ID['mraid-reference']),
|
||||
},
|
||||
{
|
||||
id: 'viewport-meta',
|
||||
severity: 'best-practice',
|
||||
title: 'Viewport meta tag not found',
|
||||
detail: 'MRAID creatives should include a mobile viewport meta tag so the ad scales predictably on mobile WebViews.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => hasViewportMeta(ctx) ? null : issue(MRAID_RULES_BY_ID['viewport-meta']),
|
||||
},
|
||||
{
|
||||
id: 'mraid-typeof-guard',
|
||||
severity: 'requirement',
|
||||
title: 'MRAID object is not guarded',
|
||||
detail: 'Guard MRAID access with typeof mraid !== "undefined" or an equivalent window.mraid guard before making MRAID calls.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => hasMraidReference(ctx) && !hasMraidTypeofGuard(ctx) ? issue(MRAID_RULES_BY_ID['mraid-typeof-guard']) : null,
|
||||
},
|
||||
{
|
||||
id: 'dom-ready-gate',
|
||||
severity: 'requirement',
|
||||
title: 'DOM readiness is not guarded',
|
||||
detail: 'MRAID startup should wait for DOM readiness as well as MRAID readiness before building or binding rich media content.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => !hasMraidReference(ctx) || hasDomReadyGate(ctx) ? null : issue(MRAID_RULES_BY_ID['dom-ready-gate']),
|
||||
},
|
||||
{
|
||||
id: 'ready-gate',
|
||||
severity: 'requirement',
|
||||
@@ -289,6 +411,30 @@ const MRAID_RULES: MraidRule[] = [
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => !hasMraidReference(ctx) || hasReadyGate(ctx) ? null : issue(MRAID_RULES_BY_ID['ready-gate']),
|
||||
},
|
||||
{
|
||||
id: 'ready-state-fallback',
|
||||
severity: 'requirement',
|
||||
title: 'MRAID ready listener/state fallback pair not found',
|
||||
detail: 'Use both mraid.addEventListener("ready", ...) and mraid.getState() so startup works when ready fires before the listener attaches.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => !hasMraidReference(ctx) || hasReadyListenerAndStateFallback(ctx) ? null : issue(MRAID_RULES_BY_ID['ready-state-fallback']),
|
||||
},
|
||||
{
|
||||
id: 'ready-timeout-fallback',
|
||||
severity: 'best-practice',
|
||||
title: 'MRAID ready timeout fallback not found',
|
||||
detail: 'A timeout fallback prevents startup hangs when a container never fires ready.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => !hasMraidReference(ctx) || hasMraidReadyTimeout(ctx) ? null : issue(MRAID_RULES_BY_ID['ready-timeout-fallback']),
|
||||
},
|
||||
{
|
||||
id: 'late-mraid-detection',
|
||||
severity: 'best-practice',
|
||||
title: 'Late MRAID injection detection not found',
|
||||
detail: 'Poll or retry briefly for late window.mraid injection before deciding the creative is running without MRAID.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => !hasMraidReference(ctx) || hasLateMraidDetection(ctx) ? null : issue(MRAID_RULES_BY_ID['late-mraid-detection']),
|
||||
},
|
||||
{
|
||||
id: 'error-listener',
|
||||
severity: 'best-practice',
|
||||
@@ -300,21 +446,45 @@ const MRAID_RULES: MraidRule[] = [
|
||||
{
|
||||
id: 'viewability',
|
||||
severity: 'best-practice',
|
||||
title: 'Viewability handling not found',
|
||||
detail: 'Use mraid.isViewable() and/or viewableChange before starting animation, audio, video, or timers.',
|
||||
title: 'MRAID viewability fallback chain not found',
|
||||
detail: 'Use exposureChange for MRAID 3.0 and keep viewableChange or isViewable() as MRAID 2.0 fallback before starting animation, audio, video, or timers.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => {
|
||||
const hasVisibilitySignal = hasMraidCall(ctx, 'isViewable') || hasEventListener(ctx, 'viewableChange') || hasEventListener(ctx, 'exposureChange');
|
||||
const hasVisibilitySignal = hasEventListener(ctx, 'exposureChange') && (hasMraidCall(ctx, 'isViewable') || hasEventListener(ctx, 'viewableChange'));
|
||||
return !hasMraidReference(ctx) || hasVisibilitySignal ? null : issue(MRAID_RULES_BY_ID['viewability']);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'audio-volume-change',
|
||||
severity: 'best-practice',
|
||||
title: 'audioVolumeChange handling not found',
|
||||
detail: 'Listen for audioVolumeChange and ignore null values; only apply volume math when the value is numeric.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => !hasMraidReference(ctx) || hasAudioVolumeNullSafeHandling(ctx) ? null : issue(MRAID_RULES_BY_ID['audio-volume-change']),
|
||||
},
|
||||
{
|
||||
id: 'open-api',
|
||||
severity: 'requirement',
|
||||
title: 'Browser navigation used instead of mraid.open',
|
||||
detail: 'Use mraid.open(url) for click-through navigation inside an MRAID ad container.',
|
||||
detail: 'Use mraid.open(url) for click-through navigation inside an MRAID ad container; avoid hyperlinks, location redirects, and unguarded browser navigation.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => usesDirectBrowserNavigation(ctx) && !hasMraidCall(ctx, 'open') ? issue(MRAID_RULES_BY_ID['open-api']) : null,
|
||||
test: (ctx) => hasUnsupportedHyperlink(ctx) || usesLocationNavigation(ctx) || (usesDirectBrowserNavigation(ctx) && !hasMraidCall(ctx, 'open')) ? issue(MRAID_RULES_BY_ID['open-api']) : null,
|
||||
},
|
||||
{
|
||||
id: 'open-guarded-fallback',
|
||||
severity: 'requirement',
|
||||
title: 'mraid.open guarded fallback not found',
|
||||
detail: 'Guard mraid.open with function/state checks and provide a window.open fallback when MRAID is unavailable or still loading.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => hasMraidCall(ctx, 'open') && (!hasMraidOpenGuard(ctx) || !hasMraidOpenLoadingGuard(ctx) || !usesDirectBrowserNavigation(ctx)) ? issue(MRAID_RULES_BY_ID['open-guarded-fallback'], mraidLine(ctx, 'open')) : null,
|
||||
},
|
||||
{
|
||||
id: 'open-video-url',
|
||||
severity: 'best-practice',
|
||||
title: 'mraid.open appears to target video media',
|
||||
detail: 'Use mraid.playVideo(url) for native video playback cases instead of mraid.open(videoUrl).',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => opensVideoUrl(ctx) && !hasMraidCall(ctx, 'playVideo') ? issue(MRAID_RULES_BY_ID['open-video-url'], mraidLine(ctx, 'open')) : null,
|
||||
},
|
||||
{
|
||||
id: 'close-api',
|
||||
@@ -327,29 +497,93 @@ const MRAID_RULES: MraidRule[] = [
|
||||
return appearsToHaveCloseUi && !hasMraidCall(ctx, 'close') ? issue(MRAID_RULES_BY_ID['close-api']) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'unload-fallback',
|
||||
severity: 'best-practice',
|
||||
title: 'No mraid.unload graceful failure path found',
|
||||
detail: 'Use mraid.unload() when the ad cannot run or refuses to show, instead of leaving a broken or blank creative.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => hasMraidReference(ctx) && !hasMraidCall(ctx, 'unload') ? issue(MRAID_RULES_BY_ID['unload-fallback']) : null,
|
||||
},
|
||||
{
|
||||
id: 'resize-properties',
|
||||
severity: 'requirement',
|
||||
title: 'mraid.resize used without setResizeProperties',
|
||||
detail: 'Call mraid.setResizeProperties(...) before mraid.resize(); otherwise the container should emit an error.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => hasMraidCall(ctx, 'resize') && !hasMraidCall(ctx, 'setResizeProperties') ? issue(MRAID_RULES_BY_ID['resize-properties'], mraidLine(ctx, 'resize')) : null,
|
||||
},
|
||||
{
|
||||
id: 'use-custom-close',
|
||||
severity: 'best-practice',
|
||||
title: 'Deprecated useCustomClose found',
|
||||
detail: 'MRAID 3.0 ignores useCustomClose; the host provides the mandatory close control.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => hasMraidCall(ctx, 'useCustomClose') || /useCustomClose/i.test(ctx.scriptText) ? issue(MRAID_RULES_BY_ID['use-custom-close'], mraidLine(ctx, 'useCustomClose')) : null,
|
||||
},
|
||||
{
|
||||
id: 'two-part-expand',
|
||||
severity: 'best-practice',
|
||||
title: 'Two-part expand appears to be used',
|
||||
detail: 'Two-part expandable ads are deprecated in MRAID 3.0; use self-contained one-part expandables.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => hasTwoPartExpand(ctx) ? issue(MRAID_RULES_BY_ID['two-part-expand'], mraidLine(ctx, 'expand')) : null,
|
||||
},
|
||||
{
|
||||
id: 'expand-support',
|
||||
severity: 'requirement',
|
||||
title: 'mraid.expand used without supports check',
|
||||
detail: 'Check mraid.supports("expand") before using expand behavior.',
|
||||
title: 'mraid.expand used without function guard',
|
||||
detail: 'Guard mraid.expand with a function check before using expand behavior.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => hasMraidCall(ctx, 'expand') && !hasSupportCheck(ctx, 'expand') ? issue(MRAID_RULES_BY_ID['expand-support'], mraidLine(ctx, 'expand')) : null,
|
||||
test: (ctx) => hasMraidCall(ctx, 'expand') && !hasMraidFunctionGuard(ctx, 'expand') ? issue(MRAID_RULES_BY_ID['expand-support'], mraidLine(ctx, 'expand')) : null,
|
||||
},
|
||||
{
|
||||
id: 'resize-support',
|
||||
severity: 'requirement',
|
||||
title: 'mraid.resize used without supports check',
|
||||
detail: 'Check mraid.supports("resize") before using resize behavior.',
|
||||
title: 'mraid.resize used without function guard',
|
||||
detail: 'Guard mraid.resize with a function check before using resize behavior.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => hasMraidCall(ctx, 'resize') && !hasSupportCheck(ctx, 'resize') ? issue(MRAID_RULES_BY_ID['resize-support'], mraidLine(ctx, 'resize')) : null,
|
||||
test: (ctx) => hasMraidCall(ctx, 'resize') && !hasMraidFunctionGuard(ctx, 'resize') ? issue(MRAID_RULES_BY_ID['resize-support'], mraidLine(ctx, 'resize')) : null,
|
||||
},
|
||||
{
|
||||
id: 'store-picture-support',
|
||||
severity: 'requirement',
|
||||
title: 'storePicture used without supports check',
|
||||
detail: 'Call mraid.supports("storePicture") before mraid.storePicture(...).',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => hasMraidCall(ctx, 'storePicture') && !hasSupportCheck(ctx, 'storePicture') ? issue(MRAID_RULES_BY_ID['store-picture-support'], mraidLine(ctx, 'storePicture')) : null,
|
||||
},
|
||||
{
|
||||
id: 'calendar-support',
|
||||
severity: 'requirement',
|
||||
title: 'createCalendarEvent used without supports check',
|
||||
detail: 'Call mraid.supports("calendar") before mraid.createCalendarEvent(...).',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => hasMraidCall(ctx, 'createCalendarEvent') && !hasSupportCheck(ctx, 'calendar') ? issue(MRAID_RULES_BY_ID['calendar-support'], mraidLine(ctx, 'createCalendarEvent')) : null,
|
||||
},
|
||||
{
|
||||
id: 'location-support',
|
||||
severity: 'requirement',
|
||||
title: 'getLocation used without supports check',
|
||||
detail: 'Call mraid.supports("location") before mraid.getLocation(). Do not use HTML5 geolocation as a substitute in MRAID.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => hasMraidCall(ctx, 'getLocation') && !hasSupportCheck(ctx, 'location') ? issue(MRAID_RULES_BY_ID['location-support'], mraidLine(ctx, 'getLocation')) : null,
|
||||
},
|
||||
{
|
||||
id: 'vpaid-support',
|
||||
severity: 'requirement',
|
||||
title: 'VPAID integration used without supports check',
|
||||
detail: 'Call mraid.supports("vpaid") before mraid.initVpaid(...).',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => hasMraidCall(ctx, 'initVpaid') && !hasSupportCheck(ctx, 'vpaid') ? issue(MRAID_RULES_BY_ID['vpaid-support'], mraidLine(ctx, 'initVpaid')) : null,
|
||||
},
|
||||
{
|
||||
id: 'orientation-support',
|
||||
severity: 'best-practice',
|
||||
title: 'Orientation properties set without supports check',
|
||||
detail: 'Check mraid.supports("orientation") before requiring orientation behavior.',
|
||||
title: 'Orientation properties set without function guard',
|
||||
detail: 'Guard mraid.setOrientationProperties with a function check before requiring orientation behavior.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => hasMraidCall(ctx, 'setOrientationProperties') && !hasSupportCheck(ctx, 'orientation') ? issue(MRAID_RULES_BY_ID['orientation-support'], mraidLine(ctx, 'setOrientationProperties')) : null,
|
||||
test: (ctx) => hasMraidCall(ctx, 'setOrientationProperties') && !hasMraidFunctionGuard(ctx, 'setOrientationProperties') ? issue(MRAID_RULES_BY_ID['orientation-support'], mraidLine(ctx, 'setOrientationProperties')) : null,
|
||||
},
|
||||
{
|
||||
id: 'size-change',
|
||||
@@ -373,6 +607,35 @@ const MRAID_RULES: MraidRule[] = [
|
||||
return changesState && !hasEventListener(ctx, 'stateChange') ? issue(MRAID_RULES_BY_ID['state-change']) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'output-script-hygiene',
|
||||
severity: 'requirement',
|
||||
title: 'Rejected script attributes found',
|
||||
detail: 'Playable output should not contain type="module" or crossorigin on script tags; ad networks can reject ES modules/CORS attributes.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => /<script\b[^>]*(\btype\s*=\s*['"]module['"]|\bcrossorigin\b)/i.test(ctx.html) ? issue(MRAID_RULES_BY_ID['output-script-hygiene']) : null,
|
||||
},
|
||||
{
|
||||
id: 'console-error',
|
||||
severity: 'best-practice',
|
||||
title: 'console.error found',
|
||||
detail: 'The ship checklist expects no console.error calls in output; use guarded logging or remove debug error logs before submission.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => /\bconsole\s*\.\s*error\s*\(/i.test(ctx.scriptText) ? issue(MRAID_RULES_BY_ID['console-error']) : null,
|
||||
},
|
||||
{
|
||||
id: 'lifecycle-stubs',
|
||||
severity: 'requirement',
|
||||
title: 'Playable lifecycle signals not found',
|
||||
detail: 'Expose or call gameReady, gameStart, gameEnd, and gameClose so network preview tools can detect the playable lifecycle.',
|
||||
reference: MRAID_REFERENCE,
|
||||
test: (ctx) => {
|
||||
const missing = ['gameReady', 'gameStart', 'gameEnd', 'gameClose'].filter(name => !hasLifecycleSignal(ctx, name));
|
||||
if (!missing.length) return null;
|
||||
const rule = MRAID_RULES_BY_ID['lifecycle-stubs'];
|
||||
return { ...issue(rule), detail: `${rule.detail} Missing: ${missing.join(', ')}.` };
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const MRAID_RULES_BY_ID = MRAID_RULES.reduce<Record<string, MraidRule>>((acc, rule) => {
|
||||
@@ -412,7 +675,7 @@ ${getToolWebviewStyles()}
|
||||
<main class="tool-page">
|
||||
<header class="tool-header">
|
||||
<h2 class="tool-title">MRAID Checker</h2>
|
||||
<p class="tool-description">Static checks for AppLovin, ironSource, and Unity folder HTML builds based on MRAID 3.0 requirements and best practices from mraidDocuments/.</p>
|
||||
<p class="tool-description">Static checks for AppLovin, ironSource, and Unity HTML builds based on GUIDE.md plus the MRAID 3.0 specification and Best Practices Guide.</p>
|
||||
</header>
|
||||
|
||||
<section class="tool-panel input-panel">
|
||||
|
||||
Reference in New Issue
Block a user