// ==UserScript== // @name Power Apps Checker formula export // @namespace https://jukkan.com // @version 0.6 // @description Intercepts Power Apps Studio authoring API traffic. Extracts formula errors (RuleErrorEvent), formula scripts, performance findings, and accessibility results. One-click export for AI coding agents. // @author Jukka Niiranen // @match https://make.powerapps.com/* // @match https://*.gateway.prod.island.powerapps.com/* // @match https://*.gateway.*.island.powerapps.com/* // @grant none // @run-at document-start // ==/UserScript== (function () { 'use strict'; // ── State ────────────────────────────────────────────────────────── const state = { formulaDiagnostics: [], // normalized from RuleErrorEvent formulaScripts: {}, // prop ID → script text (only flagged ones) accessibility: [], // from getaccessibilityerrorsasync performance: [], // from getappcheckerperformanceresponsesasync rawEventCount: 0, lastCaptureTime: null, }; const SEVERITY_MAP = { 0: 'info', 1: 'info', 2: 'warning', 3: 'warning', 4: 'error', 5: 'error' }; const TAG = '[PA-Checker]'; // ── 1. Intercept fetch() ─────────────────────────────────────────── const originalFetch = window.fetch; window.fetch = async function (...args) { const response = await originalFetch.apply(this, args); try { const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || ''; if (url.includes('/api/v2/invoke') || url.includes('/api/v1/invoke')) { const clone = response.clone(); clone.json().then(processPayload).catch(() => {}); } } catch (_) {} return response; }; // ── 2. Also intercept XHR ───────────────────────────────────────── const origXHROpen = XMLHttpRequest.prototype.open; const origXHRSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url, ...rest) { this._paUrl = url; return origXHROpen.call(this, method, url, ...rest); }; XMLHttpRequest.prototype.send = function (body) { if (this._paUrl && (this._paUrl.includes('/api/v2/invoke') || this._paUrl.includes('/api/v1/invoke'))) { this.addEventListener('load', function () { try { processPayload(JSON.parse(this.responseText)); } catch (_) {} }); } return origXHRSend.call(this, body); }; // ── 3. Process the full invoke response ─────────────────────────── function processPayload(data) { if (!Array.isArray(data)) return; let foundAnything = false; // ── Walk for RuleErrorEvent objects ── const events = []; function walkEvents(obj) { if (!obj || typeof obj !== 'object') return; if (Array.isArray(obj)) { obj.forEach(walkEvents); return; } if (obj.controlName && obj.errors && obj.propertyName) { events.push(obj); return; } Object.values(obj).forEach(walkEvents); } walkEvents(data); if (events.length > 0) { state.rawEventCount = events.length; state.formulaDiagnostics = normalizeDiagnostics(events); foundAnything = true; console.log(`${TAG} ✅ ${events.length} RuleErrorEvent(s) → ${state.formulaDiagnostics.length} diagnostics`); } // ── Flat items: prop:*:script and prop:*:hasErrorsOrWarnings ── const scripts = {}; const flagged = new Set(); for (const item of data) { if (!item || typeof item !== 'object' || Array.isArray(item)) continue; for (const [key, value] of Object.entries(item)) { if (key.includes(':script') && typeof value === 'string') { const parts = key.split(':'); if (parts.length >= 2) scripts[parts[1]] = value; } else if (key.includes(':hasErrorsOrWarnings') && value === true) { const parts = key.split(':'); if (parts.length >= 2) flagged.add(parts[1]); } } } const newScripts = {}; for (const pid of flagged) { if (scripts[pid]) newScripts[pid] = scripts[pid]; } if (Object.keys(newScripts).length > 0) { state.formulaScripts = newScripts; foundAnything = true; console.log(`${TAG} ✅ ${Object.keys(newScripts).length} formula script(s) with errors`); } // ── Named checker calls (accessibility, performance) ── for (const item of data) { if (!item?.dresult?.f || !Array.isArray(item.dresult.f) || item.dresult.f.length === 0) continue; const first = item.dresult.f[0]; if (first.accessibilityTypes !== undefined) { state.accessibility = item.dresult.f; foundAnything = true; console.log(`${TAG} ✅ ${item.dresult.f.length} accessibility issue(s)`); } else if (first.ruleName !== undefined) { state.performance = item.dresult.f; foundAnything = true; console.log(`${TAG} ✅ ${item.dresult.f.length} performance issue(s)`); } } if (foundAnything) { state.lastCaptureTime = new Date().toISOString(); showUI(); } } // ── 4. Normalize RuleErrorEvent → flat diagnostics ──────────────── function unwrapList(val) { if (Array.isArray(val)) return val; if (val && typeof val === 'object' && Array.isArray(val.f)) return val.f; return []; } function normalizeDiagnostics(events) { const out = []; for (const ev of events) { const controlName = ev.controlName || '?'; const controlId = String(ev.controlId || ''); const propertyName = ev.propertyName || ''; for (const err of unwrapList(ev.errors)) { if (!err || typeof err !== 'object') continue; const sev = err.severity ?? 2; const span = err.textSpan; out.push({ controlName, controlId, propertyName, severity: SEVERITY_MAP[sev] || 'unknown', severityNum: sev, messageKey: err.messageKey || '', shortMessage: err.shortMessage || '', longMessage: err.longMessage || '', textSpan: span ? { min: span.min, lim: span.lim } : null, howToFixMessages: unwrapList(err.howToFixMessages).filter(m => typeof m === 'string'), links: unwrapList(err.links).filter(l => l && l.url).map(l => ({ title: l.title || '', url: l.url })), }); } } return out; } // ── 5. Format: Copilot ──────────────────────────────────────────── function formatCopilot() { const d = state.formulaDiagnostics; const s = state.formulaScripts; const lines = [ 'Power Apps Studio checker results for the current canvas app.', 'Please fix the corresponding Power Fx formulas in the pa.yaml source.', '', ]; const errors = d.filter(x => x.severity === 'error'); if (errors.length) { lines.push('ERRORS (must fix):'); for (const e of errors) { lines.push(` ${e.controlName}.${e.propertyName}: ${e.shortMessage}`); if (e.howToFixMessages.length) lines.push(` Fix: ${e.howToFixMessages[0]}`); if (e.textSpan) lines.push(` At chars ${e.textSpan.min}-${e.textSpan.lim}`); } lines.push(''); } const warnings = d.filter(x => x.severity === 'warning'); if (warnings.length) { const byProp = {}; for (const w of warnings) { const k = `${w.controlName}.${w.propertyName}`; (byProp[k] ??= []).push(w); } lines.push('WARNINGS:'); for (const [k, items] of Object.entries(byProp)) { if (items.length === 1) { lines.push(` ${k}: ${items[0].shortMessage}`); } else { lines.push(` ${k}: ${items.length} issues`); for (const w of items) { const msg = w.shortMessage.length > 100 ? w.shortMessage.slice(0, 97) + '...' : w.shortMessage; lines.push(` - ${msg}`); } } } lines.push(''); } if (Object.keys(s).length) { lines.push('FORMULAS WITH ERRORS (from Studio):'); for (const [pid, script] of Object.entries(s)) { const txt = script.length > 600 ? script.slice(0, 600) + '\n... (truncated)' : script; lines.push(` prop:${pid}:`); for (const ln of txt.split('\n')) lines.push(` ${ln}`); lines.push(''); } } return lines.join('\n'); } // ── 6. Format: Markdown ─────────────────────────────────────────── function formatMarkdown() { const d = state.formulaDiagnostics; const s = state.formulaScripts; const lines = [ '# Power Apps Formula Checker Results', '', `Captured: ${state.lastCaptureTime}`, '', ]; const errors = d.filter(x => x.severity === 'error'); const warnings = d.filter(x => x.severity === 'warning'); lines.push(`**${d.length} diagnostics:** ${errors.length} errors, ${warnings.length} warnings`); if (Object.keys(s).length) lines.push(`**${Object.keys(s).length} formula scripts** with errors/warnings captured`); lines.push(''); if (errors.length) { lines.push('## Errors', ''); for (const e of errors) { lines.push(`### \`${e.controlName}.${e.propertyName}\``, ''); lines.push(`**${e.shortMessage}**`); if (e.longMessage) lines.push(`> ${e.longMessage}`); if (e.messageKey) lines.push(`Key: \`${e.messageKey}\``); if (e.textSpan) lines.push(`Span: chars ${e.textSpan.min}–${e.textSpan.lim}`); for (const fix of e.howToFixMessages) lines.push(`Fix: ${fix}`); for (const link of e.links) lines.push(`Ref: [${link.title || 'docs'}](${link.url})`); lines.push(''); } } if (warnings.length) { lines.push('## Warnings', ''); const byCtrl = {}; for (const w of warnings) { const k = `${w.controlName}.${w.propertyName}`; (byCtrl[k] ??= []).push(w); } for (const [k, items] of Object.entries(byCtrl)) { lines.push(`### \`${k}\` (${items.length} warnings)`, ''); for (const w of items) { lines.push(`- ${w.shortMessage}`); if (w.messageKey) lines.push(` Key: \`${w.messageKey}\``); } lines.push(''); } } // Performance if (state.performance.length) { lines.push('## Performance & Best Practices', ''); const byRule = {}; for (const p of state.performance) { const r = p.ruleName || 'Unknown'; (byRule[r] ??= []).push(p); } for (const [rule, items] of Object.entries(byRule)) { lines.push(`### ${rule} (${items.length})`, ''); for (const p of items) { const entity = p.entityName || p.entity || '?'; const prop = p.propertyName || ''; lines.push(`- **${entity}**${prop ? ' → `' + prop + '`' : ''}: ${p.longMessage || p.shortMessage}`); } lines.push(''); } } // Accessibility (compact) if (state.accessibility.length) { lines.push(`## Accessibility (${state.accessibility.length} issues)`, ''); const byType = {}; for (const a of state.accessibility) { const k = a.shortMessage; if (!byType[k]) byType[k] = { controls: [], fix: '' }; byType[k].controls.push(a.entity); byType[k].fix = unwrapList(a.howToFixMessages)[0] || ''; } for (const [msg, info] of Object.entries(byType)) { const preview = info.controls.slice(0, 6).join(', '); const more = info.controls.length > 6 ? ` +${info.controls.length - 6} more` : ''; lines.push(`- **${msg}** (${info.controls.length}): ${preview}${more}`); if (info.fix) lines.push(` Fix: ${info.fix}`); } lines.push(''); } // Formula scripts if (Object.keys(s).length) { lines.push('## Formula Scripts with Errors/Warnings', ''); for (const [pid, script] of Object.entries(s)) { const preview = script.length > 800 ? script.slice(0, 800) + '\n... (truncated)' : script; lines.push(`### prop:${pid}`, '', '```powerfx', preview, '```', ''); } } return lines.join('\n'); } // ── 7. Format: JSON ─────────────────────────────────────────────── function formatJSON() { return JSON.stringify({ capturedAt: state.lastCaptureTime, summary: { formulaDiagnostics: state.formulaDiagnostics.length, errors: state.formulaDiagnostics.filter(x => x.severity === 'error').length, warnings: state.formulaDiagnostics.filter(x => x.severity === 'warning').length, formulaScripts: Object.keys(state.formulaScripts).length, performanceIssues: state.performance.length, accessibilityIssues: state.accessibility.length, }, formulaDiagnostics: state.formulaDiagnostics, formulaScripts: state.formulaScripts, performance: state.performance.map(p => ({ rule: p.ruleName, entity: p.entityName || p.entity, property: p.propertyName, message: p.longMessage || p.shortMessage, })), }, null, 2); } // ── 8. Summary line for the floating UI ─────────────────────────── function summaryText() { const e = state.formulaDiagnostics.filter(x => x.severity === 'error').length; const w = state.formulaDiagnostics.filter(x => x.severity === 'warning').length; const s = Object.keys(state.formulaScripts).length; const parts = []; if (e) parts.push(`${e} err`); if (w) parts.push(`${w} warn`); if (s) parts.push(`${s} scripts`); if (state.performance.length) parts.push(`${state.performance.length} perf`); if (state.accessibility.length) parts.push(`${state.accessibility.length} a11y`); return parts.length ? parts.join(' · ') : 'No issues'; } // ── 9. Floating UI ──────────────────────────────────────────────── let uiEl = null; function showUI() { if (uiEl) { uiEl.querySelector('.pa-status').textContent = `✅ ${summaryText()}`; return; } uiEl = document.createElement('div'); uiEl.id = 'pa-checker-export'; uiEl.innerHTML = `