adaptar-vite-plugin 1.0.4 → 1.0.6
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/hmr-interceptor.d.ts +10 -0
- package/dist/hmr-interceptor.js +42 -3
- package/dist/html-tags.js +39 -3
- package/dist/scripts/error-bridge.d.ts +1 -1
- package/dist/scripts/error-bridge.js +317 -113
- package/package.json +1 -1
- package/src/hmr-interceptor.ts +83 -23
- package/src/html-tags.ts +39 -3
- package/src/scripts/error-bridge.ts +317 -113
|
@@ -7,6 +7,16 @@ export interface AdaptarErrorPayload {
|
|
|
7
7
|
colno: number | undefined;
|
|
8
8
|
source: string;
|
|
9
9
|
plugin: string | undefined;
|
|
10
|
+
classification: "render-blocking" | "diagnostic";
|
|
11
|
+
category: "compile" | "resource";
|
|
12
|
+
severity: "error" | "advisory";
|
|
13
|
+
resourceKind: "module" | "stylesheet" | "image" | "font" | "media" | "unknown";
|
|
14
|
+
renderBlocking: boolean;
|
|
15
|
+
rootHealth: {
|
|
16
|
+
state: "unknown";
|
|
17
|
+
hasRootContent: undefined;
|
|
18
|
+
readyState: undefined;
|
|
19
|
+
};
|
|
10
20
|
}
|
|
11
21
|
/**
|
|
12
22
|
* Intercepts Vite's internal WebSocket `send` to catch build/HMR errors
|
package/dist/hmr-interceptor.js
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
function classifyViteResource(filename, plugin) {
|
|
2
|
+
const signal = `${filename ?? ""} ${plugin ?? ""}`.toLowerCase();
|
|
3
|
+
if (/\.(?:css|less|sass|scss|styl)(?:$|[?#\s])/.test(signal) || /css|postcss|tailwind/.test(signal)) {
|
|
4
|
+
return "stylesheet";
|
|
5
|
+
}
|
|
6
|
+
if (/\.(?:avif|bmp|gif|ico|jpe?g|png|svg|webp)(?:$|[?#\s])/.test(signal)) {
|
|
7
|
+
return "image";
|
|
8
|
+
}
|
|
9
|
+
if (/\.(?:woff2?|ttf|otf|eot)(?:$|[?#\s])/.test(signal)) {
|
|
10
|
+
return "font";
|
|
11
|
+
}
|
|
12
|
+
if (/\.(?:aac|flac|m4a|m4v|mov|mp3|mp4|ogg|ogv|wav|webm|vtt)(?:$|[?#\s])/.test(signal)) {
|
|
13
|
+
return "media";
|
|
14
|
+
}
|
|
15
|
+
if (/\.(?:c|m)?(?:j|t)sx?(?:$|[?#\s])/.test(signal) || /react|swc|typescript|vite/.test(signal)) {
|
|
16
|
+
return "module";
|
|
17
|
+
}
|
|
18
|
+
return "unknown";
|
|
19
|
+
}
|
|
1
20
|
// How long (ms) to suppress a full-reload after a fatal error so the
|
|
2
21
|
// error overlay stays visible before the iframe clears it.
|
|
3
22
|
const RELOAD_SUPPRESSION_MS = 5_000;
|
|
@@ -22,22 +41,42 @@ export function interceptHmrErrors(server) {
|
|
|
22
41
|
// ── Fatal build / HMR error ──────────────────────────────────────────
|
|
23
42
|
if (payload.type === "error") {
|
|
24
43
|
const { err } = payload;
|
|
44
|
+
const filename = err.id ?? err.loc?.file;
|
|
45
|
+
const resourceKind = classifyViteResource(filename, err.plugin);
|
|
46
|
+
// A Vite failure in application/module compilation invalidates the
|
|
47
|
+
// candidate. Asset and stylesheet failures remain visible diagnostics
|
|
48
|
+
// because they do not prove that the application cannot render.
|
|
49
|
+
const renderBlocking = resourceKind === "module" || resourceKind === "unknown";
|
|
25
50
|
const formatted = {
|
|
26
51
|
message: `[vite] ${err.message || "Internal server error"}`,
|
|
27
52
|
stack: [err.stack, err.frame].filter(Boolean).join("\n\n"),
|
|
28
|
-
filename
|
|
53
|
+
filename,
|
|
29
54
|
lineno: err.loc?.line,
|
|
30
55
|
colno: err.loc?.column,
|
|
31
56
|
source: "vite-hmr",
|
|
32
57
|
plugin: err.plugin,
|
|
58
|
+
classification: renderBlocking ? "render-blocking" : "diagnostic",
|
|
59
|
+
category: renderBlocking ? "compile" : "resource",
|
|
60
|
+
severity: renderBlocking ? "error" : "advisory",
|
|
61
|
+
resourceKind,
|
|
62
|
+
renderBlocking,
|
|
63
|
+
rootHealth: {
|
|
64
|
+
state: "unknown",
|
|
65
|
+
hasRootContent: undefined,
|
|
66
|
+
readyState: undefined,
|
|
67
|
+
},
|
|
33
68
|
};
|
|
34
69
|
originalSend({
|
|
35
70
|
type: "custom",
|
|
36
71
|
event: "adaptar:error",
|
|
37
72
|
data: formatted,
|
|
38
73
|
});
|
|
39
|
-
//
|
|
40
|
-
|
|
74
|
+
// Only render-blocking compilation errors should interrupt Vite's
|
|
75
|
+
// normal reload path. Diagnostic asset/style failures are reported but
|
|
76
|
+
// are allowed to recover normally.
|
|
77
|
+
if (renderBlocking) {
|
|
78
|
+
suppressReloadUntil = Date.now() + RELOAD_SUPPRESSION_MS;
|
|
79
|
+
}
|
|
41
80
|
originalSend(payload);
|
|
42
81
|
return;
|
|
43
82
|
}
|
package/dist/html-tags.js
CHANGED
|
@@ -6,6 +6,16 @@ import { SELECTION_BRIDGE_SCRIPT } from "./scripts/selection-bridge.js";
|
|
|
6
6
|
*/
|
|
7
7
|
const HMR_LISTENER_SCRIPT = /* js */ `
|
|
8
8
|
const adaptarOperationId = new URL(window.location.href).searchParams.get('__adaptarOperation') || undefined;
|
|
9
|
+
let adaptarBlockingHmrError = false;
|
|
10
|
+
const readAdaptarRootHealth = () => {
|
|
11
|
+
const root = document.getElementById('root');
|
|
12
|
+
const hasRootContent = Boolean(root && (root.children.length || (root.textContent || '').trim()));
|
|
13
|
+
return {
|
|
14
|
+
state: !root ? 'missing' : (hasRootContent ? 'healthy' : 'empty'),
|
|
15
|
+
hasRootContent,
|
|
16
|
+
readyState: document.readyState,
|
|
17
|
+
};
|
|
18
|
+
};
|
|
9
19
|
const postToHost = (type, data) => {
|
|
10
20
|
try {
|
|
11
21
|
window.parent.postMessage({
|
|
@@ -19,14 +29,40 @@ const HMR_LISTENER_SCRIPT = /* js */ `
|
|
|
19
29
|
|
|
20
30
|
if (import.meta.hot) {
|
|
21
31
|
import.meta.hot.on('adaptar:error', (data) => {
|
|
22
|
-
|
|
32
|
+
if (data?.renderBlocking === true) {
|
|
33
|
+
adaptarBlockingHmrError = true;
|
|
34
|
+
}
|
|
35
|
+
postToHost('error', {
|
|
36
|
+
error: {
|
|
37
|
+
...data,
|
|
38
|
+
rootHealth: readAdaptarRootHealth(),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
23
41
|
});
|
|
24
42
|
import.meta.hot.on('adaptar:compile-success', (data) => {
|
|
25
|
-
|
|
43
|
+
const rootHealth = readAdaptarRootHealth();
|
|
44
|
+
postToHost('compile-success', {
|
|
45
|
+
...(data || {}),
|
|
46
|
+
rootHealth,
|
|
47
|
+
});
|
|
48
|
+
if (adaptarBlockingHmrError && rootHealth.hasRootContent) {
|
|
49
|
+
adaptarBlockingHmrError = false;
|
|
50
|
+
postToHost('preview-recovered', {
|
|
51
|
+
recoveredAt: Date.now(),
|
|
52
|
+
reason: 'hmr-compile-recovered',
|
|
53
|
+
resolvedSources: ['vite-hmr'],
|
|
54
|
+
hasRootContent: true,
|
|
55
|
+
rootHealth,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
26
58
|
});
|
|
27
59
|
}
|
|
28
60
|
|
|
29
|
-
postToHost('compile-success', {
|
|
61
|
+
postToHost('compile-success', {
|
|
62
|
+
compiledAt: Date.now(),
|
|
63
|
+
initial: true,
|
|
64
|
+
rootHealth: readAdaptarRootHealth(),
|
|
65
|
+
});
|
|
30
66
|
`.trim();
|
|
31
67
|
export function buildHtmlTags() {
|
|
32
68
|
return [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const ERROR_BRIDGE_SCRIPT = "(function(){\n if (window.__ADAPTAR_ERROR_BRIDGE__) return;\n window.__ADAPTAR_ERROR_BRIDGE__ = true;\n\n var operationId = new URL(window.location.href).searchParams.get('__adaptarOperation') || undefined;\n\n function sendRendered(heartbeat) {\n try {\n var root = document.getElementById('root');\n window.parent.postMessage({\n source: 'adaptar-preview',\n type: heartbeat ? 'render-heartbeat' : 'preview-rendered',\n operationId: operationId,\n renderedAt: Date.now(),\n hasRootContent: Boolean(root && (root.children.length || (root.textContent || '').trim()))\n }, '*');\n } catch (_) {}\n }\n\n function send(message, stack, filename, lineno, colno, source, plugin) {\n try {\n window.parent.postMessage({\n source: 'adaptar-preview',\n type: 'error',\n operationId: operationId,\n error: {\n message: message || 'Unknown error',\n stack: stack || '',\n filename: filename,\n lineno: lineno,\n colno: colno,\n source: source,\n plugin: plugin\n }\n }, '*');\n } catch (_) {}\n }\n\n // Resource load errors (scripts, stylesheets)\n window.addEventListener('error', function(e) {\n var target = e.target || e.srcElement;\n if (target && target !== window && target.tagName) {\n var tag = String(target.tagName).toLowerCase();\n var url = target.src || target.href;\n if (url && (tag === 'script' || tag === 'link' || tag === 'img')) {\n send(\n (tag === 'link'\n ? 'Failed to load stylesheet: '\n : tag === 'img'\n ? 'Failed to load image: '\n : 'Failed to load module: ') + url,\n '', url, undefined, undefined, 'resource-load'\n );\n return;\n }\n }\n send(\n e.message || (e.error && e.error.message) || 'Runtime error occurred',\n e.error ? e.error.stack : '',\n e.filename, e.lineno, e.colno, 'runtime'\n );\n });\n\n // Unhandled promise rejections\n window.addEventListener('unhandledrejection', function(e) {\n var r = e.reason;\n send(\n r && r.message ? r.message : String(r || 'Unhandled Promise Rejection'),\n r && r.stack,\n r && r.fileName,\n r && r.lineNumber,\n r && r.columnNumber,\n 'unhandledrejection'\n );\n });\n\n // Console error interception for silent Vite/Syntax errors\n var nativeConsoleError = console.error;\n console.error = function() {\n var args = Array.prototype.slice.call(arguments);\n var msg = args.map(function(a) {\n return typeof a === 'string' ? a : (a && a.message ? a.message : String(a));\n }).join(' ');\n if (/Failed to load module|Internal Server Error|Syntax Error/i.test(msg)) {\n send(msg, '', undefined, undefined, undefined, 'console.error');\n }\n return nativeConsoleError.apply(console, args);\n };\n\n // Blank screen detector\n window.addEventListener('load', function() {\n requestAnimationFrame(function() {\n requestAnimationFrame(function() {\n sendRendered(false);\n });\n });\n\n setTimeout(function() {\n var root = document.getElementById('root');\n if (root && root.children.length === 0 && !(root.textContent || '').trim()) {\n send(\n 'Runtime error: Preview failed to render; a component or import may have failed silently.',\n '', location.href, undefined, undefined, 'blank-screen'\n );\n } else {\n var images = Array.prototype.slice.call(document.querySelectorAll('img'));\n var controls = Array.prototype.slice.call(\n document.querySelectorAll('button, a[href], input, select, textarea')\n );\n var checks = {\n viewportWidth: document.documentElement.clientWidth || window.innerWidth || 0,\n horizontalOverflow:\n document.documentElement.scrollWidth >\n (document.documentElement.clientWidth || window.innerWidth || 0) + 2,\n brokenImages: images.filter(function(img) {\n return img.complete && img.naturalWidth === 0;\n }).length,\n missingImageAlt: images.filter(function(img) {\n return !img.hasAttribute('alt');\n }).length,\n unlabeledControls: controls.filter(function(control) {\n var label = (\n control.getAttribute('aria-label') ||\n control.getAttribute('title') ||\n control.textContent ||\n control.value ||\n ''\n ).trim();\n return !label;\n }).length\n };\n window.parent.postMessage({\n source: 'adaptar-preview',\n type: 'preview-stable',\n operationId: operationId,\n stableAt: Date.now(),\n hasRootContent: Boolean(root && (root.children.length || (root.textContent || '').trim())),\n checks: checks\n }, '*');\n }\n }, 3000);\n });\n\n window.setInterval(function() { sendRendered(true); }, 20000);\n})();";
|
|
1
|
+
export declare const ERROR_BRIDGE_SCRIPT = "(function(){\n if (window.__ADAPTAR_ERROR_BRIDGE__) return;\n window.__ADAPTAR_ERROR_BRIDGE__ = true;\n\n var operationId = new URL(window.location.href).searchParams.get('__adaptarOperation') || undefined;\n var activeBlockingSources = Object.create(null);\n var blankCheckStartedAt = 0;\n var blankCheckTimer;\n var blankFailureReported = false;\n var BLANK_CONFIRMATION_MS = 8000;\n var ROOT_CHECK_INTERVAL_MS = 500;\n\n function readRootHealth() {\n var root = document.getElementById('root');\n var hasRootContent = Boolean(\n root && (root.children.length || (root.textContent || '').trim())\n );\n\n return {\n state: !root ? 'missing' : (hasRootContent ? 'healthy' : 'empty'),\n hasRootContent: hasRootContent,\n readyState: document.readyState\n };\n }\n\n function sendRendered(heartbeat) {\n try {\n var rootHealth = readRootHealth();\n window.parent.postMessage({\n source: 'adaptar-preview',\n type: heartbeat ? 'render-heartbeat' : 'preview-rendered',\n operationId: operationId,\n renderedAt: Date.now(),\n hasRootContent: rootHealth.hasRootContent,\n rootHealth: rootHealth\n }, '*');\n } catch (_) {}\n }\n\n function classifyResource(target, url) {\n var tag = String((target && target.tagName) || '').toLowerCase();\n var rel = String((target && target.rel) || '').toLowerCase();\n var as = String((target && target.as) || '').toLowerCase();\n var type = String((target && target.type) || '').toLowerCase();\n var value = String(url || '').split(/[?#]/)[0].toLowerCase();\n\n if (\n as === 'font' ||\n type.indexOf('font/') === 0 ||\n /\\.(woff2?|ttf|otf|eot)$/.test(value)\n ) return 'font';\n if (\n tag === 'img' ||\n as === 'image' ||\n /\\.(avif|bmp|gif|ico|jpe?g|png|svg|webp)$/.test(value)\n ) return 'image';\n if (\n tag === 'audio' || tag === 'video' || tag === 'source' || tag === 'track' ||\n as === 'audio' || as === 'video' ||\n /\\.(aac|flac|m4a|m4v|mov|mp3|mp4|ogg|ogv|wav|webm|vtt)$/.test(value)\n ) return 'media';\n if (\n tag === 'script' ||\n as === 'script' ||\n rel === 'modulepreload' ||\n type === 'module' ||\n /\\.(c|m)?(j|t)sx?$/.test(value)\n ) return 'module';\n if (\n (tag === 'link' && rel === 'stylesheet') ||\n as === 'style' ||\n type === 'text/css' ||\n /\\.(css|less|sass|scss|styl)$/.test(value)\n ) return 'stylesheet';\n return 'unknown';\n }\n\n function resourceFailureMessage(resourceKind, url) {\n var labels = {\n font: 'Failed to load font: ',\n image: 'Failed to load image: ',\n media: 'Failed to load media: ',\n module: 'Failed to load module: ',\n stylesheet: 'Failed to load stylesheet: ',\n unknown: 'Failed to load resource: '\n };\n return (labels[resourceKind] || labels.unknown) + url;\n }\n\n function send(message, stack, filename, lineno, colno, source, plugin, metadata) {\n try {\n var details = metadata || {};\n var rootHealth = readRootHealth();\n var renderBlocking = details.renderBlocking === true || (\n details.blockWhenRootEmpty === true && !rootHealth.hasRootContent\n );\n var errorSource = source || 'unknown';\n\n if (renderBlocking) {\n activeBlockingSources[errorSource] = true;\n }\n\n window.parent.postMessage({\n source: 'adaptar-preview',\n type: 'error',\n operationId: operationId,\n error: {\n message: message || 'Unknown error',\n stack: stack || '',\n filename: filename,\n lineno: lineno,\n colno: colno,\n source: source,\n plugin: plugin,\n classification: renderBlocking ? 'render-blocking' : 'diagnostic',\n category: details.category || 'runtime',\n severity: renderBlocking ? 'error' : 'advisory',\n resourceKind: details.resourceKind || 'unknown',\n renderBlocking: renderBlocking,\n rootHealth: rootHealth\n }\n }, '*');\n } catch (_) {}\n }\n\n function recoverBlockingErrorsIfRendered(reason) {\n try {\n var rootHealth = readRootHealth();\n var resolvedSources = Object.keys(activeBlockingSources);\n if (!rootHealth.hasRootContent || resolvedSources.length === 0) return;\n\n activeBlockingSources = Object.create(null);\n window.parent.postMessage({\n source: 'adaptar-preview',\n type: 'preview-recovered',\n operationId: operationId,\n recoveredAt: Date.now(),\n reason: reason || 'root-healthy',\n resolvedSources: resolvedSources,\n hasRootContent: true,\n rootHealth: rootHealth\n }, '*');\n } catch (_) {}\n }\n\n // Resource failures remain observable, but only a module required before\n // the application mounts can be classified as render-blocking. Images,\n // fonts, media, and stylesheets are quality diagnostics.\n window.addEventListener('error', function(e) {\n var target = e.target || e.srcElement;\n if (target && target !== window && target.tagName) {\n var url = target.src || target.href || target.currentSrc;\n var resourceKind = classifyResource(target, url);\n if (url || resourceKind !== 'unknown') {\n send(\n resourceFailureMessage(resourceKind, url || '(unknown resource URL)'),\n '', url, undefined, undefined, 'resource-load', undefined,\n {\n category: 'resource',\n resourceKind: resourceKind,\n blockWhenRootEmpty: resourceKind === 'module'\n }\n );\n return;\n }\n }\n\n send(\n e.message || (e.error && e.error.message) || 'Runtime error occurred',\n e.error ? e.error.stack : '',\n e.filename, e.lineno, e.colno, 'runtime', undefined,\n { category: 'runtime', resourceKind: 'module', blockWhenRootEmpty: true }\n );\n }, true);\n\n // An async failure is blocking only when the application has no rendered\n // root. Interaction failures in an otherwise-rendered page remain visible\n // diagnostics and never invalidate the candidate by themselves.\n window.addEventListener('unhandledrejection', function(e) {\n var r = e.reason;\n send(\n r && r.message ? r.message : String(r || 'Unhandled Promise Rejection'),\n r && r.stack,\n r && r.fileName,\n r && r.lineNumber,\n r && r.columnNumber,\n 'unhandledrejection', undefined,\n { category: 'runtime', resourceKind: 'module', blockWhenRootEmpty: true }\n );\n });\n\n // Intercept only errors that indicate a Vite/module compilation failure.\n // Their render-blocking status is still tied to an empty application root;\n // Vite HMR compile failures are classified separately by the server bridge.\n var nativeConsoleError = console.error;\n console.error = function() {\n var args = Array.prototype.slice.call(arguments);\n var msg = args.map(function(a) {\n return typeof a === 'string' ? a : (a && a.message ? a.message : String(a));\n }).join(' ');\n if (/Failed to load module|Internal Server Error|Syntax Error/i.test(msg)) {\n send(\n msg, '', undefined, undefined, undefined, 'console.error', undefined,\n { category: 'compile', resourceKind: 'module', blockWhenRootEmpty: true }\n );\n }\n return nativeConsoleError.apply(console, args);\n };\n\n function sendStable(rootHealth) {\n var images = Array.prototype.slice.call(document.querySelectorAll('img'));\n var controls = Array.prototype.slice.call(\n document.querySelectorAll('button, a[href], input, select, textarea')\n );\n var checks = {\n viewportWidth: document.documentElement.clientWidth || window.innerWidth || 0,\n horizontalOverflow:\n document.documentElement.scrollWidth >\n (document.documentElement.clientWidth || window.innerWidth || 0) + 2,\n brokenImages: images.filter(function(img) {\n return img.complete && img.naturalWidth === 0;\n }).length,\n missingImageAlt: images.filter(function(img) {\n return !img.hasAttribute('alt');\n }).length,\n unlabeledControls: controls.filter(function(control) {\n var label = (\n control.getAttribute('aria-label') ||\n control.getAttribute('title') ||\n control.textContent ||\n control.value ||\n ''\n ).trim();\n return !label;\n }).length\n };\n var diagnostics = [];\n\n if (checks.horizontalOverflow) {\n diagnostics.push({\n code: 'horizontal-overflow',\n classification: 'diagnostic',\n category: 'layout',\n severity: 'advisory',\n resourceKind: 'quality',\n renderBlocking: false,\n message: 'The preview has horizontal overflow.'\n });\n }\n if (checks.brokenImages > 0) {\n diagnostics.push({\n code: 'broken-images',\n classification: 'diagnostic',\n category: 'asset',\n severity: 'advisory',\n resourceKind: 'image',\n renderBlocking: false,\n message: 'One or more images could not be displayed.',\n count: checks.brokenImages\n });\n }\n if (checks.missingImageAlt > 0) {\n diagnostics.push({\n code: 'missing-image-alt',\n classification: 'diagnostic',\n category: 'accessibility',\n severity: 'advisory',\n resourceKind: 'image',\n renderBlocking: false,\n message: 'One or more images are missing alternative text.',\n count: checks.missingImageAlt\n });\n }\n if (checks.unlabeledControls > 0) {\n diagnostics.push({\n code: 'unlabeled-controls',\n classification: 'diagnostic',\n category: 'accessibility',\n severity: 'advisory',\n resourceKind: 'control',\n renderBlocking: false,\n message: 'One or more controls do not have an accessible label.',\n count: checks.unlabeledControls\n });\n }\n\n window.parent.postMessage({\n source: 'adaptar-preview',\n type: 'preview-stable',\n operationId: operationId,\n stableAt: Date.now(),\n hasRootContent: rootHealth.hasRootContent,\n rootHealth: rootHealth,\n checks: checks,\n diagnostics: diagnostics\n }, '*');\n }\n\n function confirmPreviewHealth() {\n var rootHealth = readRootHealth();\n if (rootHealth.hasRootContent) {\n if (blankCheckTimer) {\n clearTimeout(blankCheckTimer);\n blankCheckTimer = undefined;\n }\n blankCheckStartedAt = 0;\n blankFailureReported = false;\n recoverBlockingErrorsIfRendered('root-recovered');\n sendStable(rootHealth);\n return;\n }\n\n if (!blankCheckStartedAt) blankCheckStartedAt = Date.now();\n if (Date.now() - blankCheckStartedAt >= BLANK_CONFIRMATION_MS) {\n if (!blankFailureReported) {\n blankFailureReported = true;\n send(\n 'Runtime error: Preview failed to render; a component or import may have failed silently.',\n '', location.href, undefined, undefined, 'blank-screen', undefined,\n { category: 'render', resourceKind: 'document', renderBlocking: true }\n );\n }\n blankCheckTimer = setTimeout(confirmPreviewHealth, ROOT_CHECK_INTERVAL_MS);\n return;\n }\n\n blankCheckTimer = setTimeout(confirmPreviewHealth, ROOT_CHECK_INTERVAL_MS);\n }\n\n // A blank root is blocking only when it remains blank across a sustained\n // observation window. Normal React mounting and Vite reloads are transient.\n window.addEventListener('load', function() {\n blankCheckStartedAt = Date.now();\n requestAnimationFrame(function() {\n requestAnimationFrame(function() {\n sendRendered(false);\n });\n });\n\n setTimeout(confirmPreviewHealth, 3000);\n });\n\n window.setInterval(function() {\n sendRendered(true);\n }, 20000);\n window.setInterval(function() {\n recoverBlockingErrorsIfRendered('root-health-monitor');\n }, 1000);\n})();";
|
|
@@ -1,145 +1,349 @@
|
|
|
1
|
-
export const ERROR_BRIDGE_SCRIPT = /* js */ `(function(){
|
|
1
|
+
export const ERROR_BRIDGE_SCRIPT = /* js */ `(function(){
|
|
2
2
|
if (window.__ADAPTAR_ERROR_BRIDGE__) return;
|
|
3
3
|
window.__ADAPTAR_ERROR_BRIDGE__ = true;
|
|
4
4
|
|
|
5
5
|
var operationId = new URL(window.location.href).searchParams.get('__adaptarOperation') || undefined;
|
|
6
|
+
var activeBlockingSources = Object.create(null);
|
|
7
|
+
var blankCheckStartedAt = 0;
|
|
8
|
+
var blankCheckTimer;
|
|
9
|
+
var blankFailureReported = false;
|
|
10
|
+
var BLANK_CONFIRMATION_MS = 8000;
|
|
11
|
+
var ROOT_CHECK_INTERVAL_MS = 500;
|
|
12
|
+
|
|
13
|
+
function readRootHealth() {
|
|
14
|
+
var root = document.getElementById('root');
|
|
15
|
+
var hasRootContent = Boolean(
|
|
16
|
+
root && (root.children.length || (root.textContent || '').trim())
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
state: !root ? 'missing' : (hasRootContent ? 'healthy' : 'empty'),
|
|
21
|
+
hasRootContent: hasRootContent,
|
|
22
|
+
readyState: document.readyState
|
|
23
|
+
};
|
|
24
|
+
}
|
|
6
25
|
|
|
7
26
|
function sendRendered(heartbeat) {
|
|
8
27
|
try {
|
|
9
|
-
var
|
|
28
|
+
var rootHealth = readRootHealth();
|
|
10
29
|
window.parent.postMessage({
|
|
11
30
|
source: 'adaptar-preview',
|
|
12
31
|
type: heartbeat ? 'render-heartbeat' : 'preview-rendered',
|
|
13
32
|
operationId: operationId,
|
|
14
33
|
renderedAt: Date.now(),
|
|
15
|
-
hasRootContent:
|
|
34
|
+
hasRootContent: rootHealth.hasRootContent,
|
|
35
|
+
rootHealth: rootHealth
|
|
16
36
|
}, '*');
|
|
17
37
|
} catch (_) {}
|
|
18
38
|
}
|
|
19
|
-
|
|
20
|
-
function
|
|
21
|
-
|
|
39
|
+
|
|
40
|
+
function classifyResource(target, url) {
|
|
41
|
+
var tag = String((target && target.tagName) || '').toLowerCase();
|
|
42
|
+
var rel = String((target && target.rel) || '').toLowerCase();
|
|
43
|
+
var as = String((target && target.as) || '').toLowerCase();
|
|
44
|
+
var type = String((target && target.type) || '').toLowerCase();
|
|
45
|
+
var value = String(url || '').split(/[?#]/)[0].toLowerCase();
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
as === 'font' ||
|
|
49
|
+
type.indexOf('font/') === 0 ||
|
|
50
|
+
/\\.(woff2?|ttf|otf|eot)$/.test(value)
|
|
51
|
+
) return 'font';
|
|
52
|
+
if (
|
|
53
|
+
tag === 'img' ||
|
|
54
|
+
as === 'image' ||
|
|
55
|
+
/\\.(avif|bmp|gif|ico|jpe?g|png|svg|webp)$/.test(value)
|
|
56
|
+
) return 'image';
|
|
57
|
+
if (
|
|
58
|
+
tag === 'audio' || tag === 'video' || tag === 'source' || tag === 'track' ||
|
|
59
|
+
as === 'audio' || as === 'video' ||
|
|
60
|
+
/\\.(aac|flac|m4a|m4v|mov|mp3|mp4|ogg|ogv|wav|webm|vtt)$/.test(value)
|
|
61
|
+
) return 'media';
|
|
62
|
+
if (
|
|
63
|
+
tag === 'script' ||
|
|
64
|
+
as === 'script' ||
|
|
65
|
+
rel === 'modulepreload' ||
|
|
66
|
+
type === 'module' ||
|
|
67
|
+
/\\.(c|m)?(j|t)sx?$/.test(value)
|
|
68
|
+
) return 'module';
|
|
69
|
+
if (
|
|
70
|
+
(tag === 'link' && rel === 'stylesheet') ||
|
|
71
|
+
as === 'style' ||
|
|
72
|
+
type === 'text/css' ||
|
|
73
|
+
/\\.(css|less|sass|scss|styl)$/.test(value)
|
|
74
|
+
) return 'stylesheet';
|
|
75
|
+
return 'unknown';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resourceFailureMessage(resourceKind, url) {
|
|
79
|
+
var labels = {
|
|
80
|
+
font: 'Failed to load font: ',
|
|
81
|
+
image: 'Failed to load image: ',
|
|
82
|
+
media: 'Failed to load media: ',
|
|
83
|
+
module: 'Failed to load module: ',
|
|
84
|
+
stylesheet: 'Failed to load stylesheet: ',
|
|
85
|
+
unknown: 'Failed to load resource: '
|
|
86
|
+
};
|
|
87
|
+
return (labels[resourceKind] || labels.unknown) + url;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function send(message, stack, filename, lineno, colno, source, plugin, metadata) {
|
|
91
|
+
try {
|
|
92
|
+
var details = metadata || {};
|
|
93
|
+
var rootHealth = readRootHealth();
|
|
94
|
+
var renderBlocking = details.renderBlocking === true || (
|
|
95
|
+
details.blockWhenRootEmpty === true && !rootHealth.hasRootContent
|
|
96
|
+
);
|
|
97
|
+
var errorSource = source || 'unknown';
|
|
98
|
+
|
|
99
|
+
if (renderBlocking) {
|
|
100
|
+
activeBlockingSources[errorSource] = true;
|
|
101
|
+
}
|
|
102
|
+
|
|
22
103
|
window.parent.postMessage({
|
|
23
104
|
source: 'adaptar-preview',
|
|
24
105
|
type: 'error',
|
|
25
106
|
operationId: operationId,
|
|
26
107
|
error: {
|
|
27
|
-
message: message || 'Unknown error',
|
|
28
|
-
stack: stack || '',
|
|
29
|
-
filename: filename,
|
|
30
|
-
lineno: lineno,
|
|
31
|
-
colno: colno,
|
|
32
|
-
source: source,
|
|
33
|
-
plugin: plugin
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
108
|
+
message: message || 'Unknown error',
|
|
109
|
+
stack: stack || '',
|
|
110
|
+
filename: filename,
|
|
111
|
+
lineno: lineno,
|
|
112
|
+
colno: colno,
|
|
113
|
+
source: source,
|
|
114
|
+
plugin: plugin,
|
|
115
|
+
classification: renderBlocking ? 'render-blocking' : 'diagnostic',
|
|
116
|
+
category: details.category || 'runtime',
|
|
117
|
+
severity: renderBlocking ? 'error' : 'advisory',
|
|
118
|
+
resourceKind: details.resourceKind || 'unknown',
|
|
119
|
+
renderBlocking: renderBlocking,
|
|
120
|
+
rootHealth: rootHealth
|
|
121
|
+
}
|
|
122
|
+
}, '*');
|
|
123
|
+
} catch (_) {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function recoverBlockingErrorsIfRendered(reason) {
|
|
127
|
+
try {
|
|
128
|
+
var rootHealth = readRootHealth();
|
|
129
|
+
var resolvedSources = Object.keys(activeBlockingSources);
|
|
130
|
+
if (!rootHealth.hasRootContent || resolvedSources.length === 0) return;
|
|
131
|
+
|
|
132
|
+
activeBlockingSources = Object.create(null);
|
|
133
|
+
window.parent.postMessage({
|
|
134
|
+
source: 'adaptar-preview',
|
|
135
|
+
type: 'preview-recovered',
|
|
136
|
+
operationId: operationId,
|
|
137
|
+
recoveredAt: Date.now(),
|
|
138
|
+
reason: reason || 'root-healthy',
|
|
139
|
+
resolvedSources: resolvedSources,
|
|
140
|
+
hasRootContent: true,
|
|
141
|
+
rootHealth: rootHealth
|
|
142
|
+
}, '*');
|
|
143
|
+
} catch (_) {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Resource failures remain observable, but only a module required before
|
|
147
|
+
// the application mounts can be classified as render-blocking. Images,
|
|
148
|
+
// fonts, media, and stylesheets are quality diagnostics.
|
|
149
|
+
window.addEventListener('error', function(e) {
|
|
150
|
+
var target = e.target || e.srcElement;
|
|
151
|
+
if (target && target !== window && target.tagName) {
|
|
152
|
+
var url = target.src || target.href || target.currentSrc;
|
|
153
|
+
var resourceKind = classifyResource(target, url);
|
|
154
|
+
if (url || resourceKind !== 'unknown') {
|
|
155
|
+
send(
|
|
156
|
+
resourceFailureMessage(resourceKind, url || '(unknown resource URL)'),
|
|
157
|
+
'', url, undefined, undefined, 'resource-load', undefined,
|
|
158
|
+
{
|
|
159
|
+
category: 'resource',
|
|
160
|
+
resourceKind: resourceKind,
|
|
161
|
+
blockWhenRootEmpty: resourceKind === 'module'
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
send(
|
|
169
|
+
e.message || (e.error && e.error.message) || 'Runtime error occurred',
|
|
170
|
+
e.error ? e.error.stack : '',
|
|
171
|
+
e.filename, e.lineno, e.colno, 'runtime', undefined,
|
|
172
|
+
{ category: 'runtime', resourceKind: 'module', blockWhenRootEmpty: true }
|
|
173
|
+
);
|
|
174
|
+
}, true);
|
|
175
|
+
|
|
176
|
+
// An async failure is blocking only when the application has no rendered
|
|
177
|
+
// root. Interaction failures in an otherwise-rendered page remain visible
|
|
178
|
+
// diagnostics and never invalidate the candidate by themselves.
|
|
179
|
+
window.addEventListener('unhandledrejection', function(e) {
|
|
180
|
+
var r = e.reason;
|
|
181
|
+
send(
|
|
182
|
+
r && r.message ? r.message : String(r || 'Unhandled Promise Rejection'),
|
|
183
|
+
r && r.stack,
|
|
184
|
+
r && r.fileName,
|
|
185
|
+
r && r.lineNumber,
|
|
186
|
+
r && r.columnNumber,
|
|
187
|
+
'unhandledrejection', undefined,
|
|
188
|
+
{ category: 'runtime', resourceKind: 'module', blockWhenRootEmpty: true }
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Intercept only errors that indicate a Vite/module compilation failure.
|
|
193
|
+
// Their render-blocking status is still tied to an empty application root;
|
|
194
|
+
// Vite HMR compile failures are classified separately by the server bridge.
|
|
195
|
+
var nativeConsoleError = console.error;
|
|
196
|
+
console.error = function() {
|
|
197
|
+
var args = Array.prototype.slice.call(arguments);
|
|
198
|
+
var msg = args.map(function(a) {
|
|
199
|
+
return typeof a === 'string' ? a : (a && a.message ? a.message : String(a));
|
|
200
|
+
}).join(' ');
|
|
201
|
+
if (/Failed to load module|Internal Server Error|Syntax Error/i.test(msg)) {
|
|
202
|
+
send(
|
|
203
|
+
msg, '', undefined, undefined, undefined, 'console.error', undefined,
|
|
204
|
+
{ category: 'compile', resourceKind: 'module', blockWhenRootEmpty: true }
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return nativeConsoleError.apply(console, args);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
function sendStable(rootHealth) {
|
|
211
|
+
var images = Array.prototype.slice.call(document.querySelectorAll('img'));
|
|
212
|
+
var controls = Array.prototype.slice.call(
|
|
213
|
+
document.querySelectorAll('button, a[href], input, select, textarea')
|
|
214
|
+
);
|
|
215
|
+
var checks = {
|
|
216
|
+
viewportWidth: document.documentElement.clientWidth || window.innerWidth || 0,
|
|
217
|
+
horizontalOverflow:
|
|
218
|
+
document.documentElement.scrollWidth >
|
|
219
|
+
(document.documentElement.clientWidth || window.innerWidth || 0) + 2,
|
|
220
|
+
brokenImages: images.filter(function(img) {
|
|
221
|
+
return img.complete && img.naturalWidth === 0;
|
|
222
|
+
}).length,
|
|
223
|
+
missingImageAlt: images.filter(function(img) {
|
|
224
|
+
return !img.hasAttribute('alt');
|
|
225
|
+
}).length,
|
|
226
|
+
unlabeledControls: controls.filter(function(control) {
|
|
227
|
+
var label = (
|
|
228
|
+
control.getAttribute('aria-label') ||
|
|
229
|
+
control.getAttribute('title') ||
|
|
230
|
+
control.textContent ||
|
|
231
|
+
control.value ||
|
|
232
|
+
''
|
|
233
|
+
).trim();
|
|
234
|
+
return !label;
|
|
235
|
+
}).length
|
|
236
|
+
};
|
|
237
|
+
var diagnostics = [];
|
|
238
|
+
|
|
239
|
+
if (checks.horizontalOverflow) {
|
|
240
|
+
diagnostics.push({
|
|
241
|
+
code: 'horizontal-overflow',
|
|
242
|
+
classification: 'diagnostic',
|
|
243
|
+
category: 'layout',
|
|
244
|
+
severity: 'advisory',
|
|
245
|
+
resourceKind: 'quality',
|
|
246
|
+
renderBlocking: false,
|
|
247
|
+
message: 'The preview has horizontal overflow.'
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (checks.brokenImages > 0) {
|
|
251
|
+
diagnostics.push({
|
|
252
|
+
code: 'broken-images',
|
|
253
|
+
classification: 'diagnostic',
|
|
254
|
+
category: 'asset',
|
|
255
|
+
severity: 'advisory',
|
|
256
|
+
resourceKind: 'image',
|
|
257
|
+
renderBlocking: false,
|
|
258
|
+
message: 'One or more images could not be displayed.',
|
|
259
|
+
count: checks.brokenImages
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
if (checks.missingImageAlt > 0) {
|
|
263
|
+
diagnostics.push({
|
|
264
|
+
code: 'missing-image-alt',
|
|
265
|
+
classification: 'diagnostic',
|
|
266
|
+
category: 'accessibility',
|
|
267
|
+
severity: 'advisory',
|
|
268
|
+
resourceKind: 'image',
|
|
269
|
+
renderBlocking: false,
|
|
270
|
+
message: 'One or more images are missing alternative text.',
|
|
271
|
+
count: checks.missingImageAlt
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (checks.unlabeledControls > 0) {
|
|
275
|
+
diagnostics.push({
|
|
276
|
+
code: 'unlabeled-controls',
|
|
277
|
+
classification: 'diagnostic',
|
|
278
|
+
category: 'accessibility',
|
|
279
|
+
severity: 'advisory',
|
|
280
|
+
resourceKind: 'control',
|
|
281
|
+
renderBlocking: false,
|
|
282
|
+
message: 'One or more controls do not have an accessible label.',
|
|
283
|
+
count: checks.unlabeledControls
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
window.parent.postMessage({
|
|
288
|
+
source: 'adaptar-preview',
|
|
289
|
+
type: 'preview-stable',
|
|
290
|
+
operationId: operationId,
|
|
291
|
+
stableAt: Date.now(),
|
|
292
|
+
hasRootContent: rootHealth.hasRootContent,
|
|
293
|
+
rootHealth: rootHealth,
|
|
294
|
+
checks: checks,
|
|
295
|
+
diagnostics: diagnostics
|
|
296
|
+
}, '*');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function confirmPreviewHealth() {
|
|
300
|
+
var rootHealth = readRootHealth();
|
|
301
|
+
if (rootHealth.hasRootContent) {
|
|
302
|
+
if (blankCheckTimer) {
|
|
303
|
+
clearTimeout(blankCheckTimer);
|
|
304
|
+
blankCheckTimer = undefined;
|
|
305
|
+
}
|
|
306
|
+
blankCheckStartedAt = 0;
|
|
307
|
+
blankFailureReported = false;
|
|
308
|
+
recoverBlockingErrorsIfRendered('root-recovered');
|
|
309
|
+
sendStable(rootHealth);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!blankCheckStartedAt) blankCheckStartedAt = Date.now();
|
|
314
|
+
if (Date.now() - blankCheckStartedAt >= BLANK_CONFIRMATION_MS) {
|
|
315
|
+
if (!blankFailureReported) {
|
|
316
|
+
blankFailureReported = true;
|
|
46
317
|
send(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
? 'Failed to load image: '
|
|
51
|
-
: 'Failed to load module: ') + url,
|
|
52
|
-
'', url, undefined, undefined, 'resource-load'
|
|
318
|
+
'Runtime error: Preview failed to render; a component or import may have failed silently.',
|
|
319
|
+
'', location.href, undefined, undefined, 'blank-screen', undefined,
|
|
320
|
+
{ category: 'render', resourceKind: 'document', renderBlocking: true }
|
|
53
321
|
);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// Unhandled promise rejections
|
|
65
|
-
window.addEventListener('unhandledrejection', function(e) {
|
|
66
|
-
var r = e.reason;
|
|
67
|
-
send(
|
|
68
|
-
r && r.message ? r.message : String(r || 'Unhandled Promise Rejection'),
|
|
69
|
-
r && r.stack,
|
|
70
|
-
r && r.fileName,
|
|
71
|
-
r && r.lineNumber,
|
|
72
|
-
r && r.columnNumber,
|
|
73
|
-
'unhandledrejection'
|
|
74
|
-
);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Console error interception for silent Vite/Syntax errors
|
|
78
|
-
var nativeConsoleError = console.error;
|
|
79
|
-
console.error = function() {
|
|
80
|
-
var args = Array.prototype.slice.call(arguments);
|
|
81
|
-
var msg = args.map(function(a) {
|
|
82
|
-
return typeof a === 'string' ? a : (a && a.message ? a.message : String(a));
|
|
83
|
-
}).join(' ');
|
|
84
|
-
if (/Failed to load module|Internal Server Error|Syntax Error/i.test(msg)) {
|
|
85
|
-
send(msg, '', undefined, undefined, undefined, 'console.error');
|
|
86
|
-
}
|
|
87
|
-
return nativeConsoleError.apply(console, args);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// Blank screen detector
|
|
322
|
+
}
|
|
323
|
+
blankCheckTimer = setTimeout(confirmPreviewHealth, ROOT_CHECK_INTERVAL_MS);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
blankCheckTimer = setTimeout(confirmPreviewHealth, ROOT_CHECK_INTERVAL_MS);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// A blank root is blocking only when it remains blank across a sustained
|
|
331
|
+
// observation window. Normal React mounting and Vite reloads are transient.
|
|
91
332
|
window.addEventListener('load', function() {
|
|
333
|
+
blankCheckStartedAt = Date.now();
|
|
92
334
|
requestAnimationFrame(function() {
|
|
93
335
|
requestAnimationFrame(function() {
|
|
94
336
|
sendRendered(false);
|
|
95
337
|
});
|
|
96
338
|
});
|
|
97
339
|
|
|
98
|
-
setTimeout(
|
|
99
|
-
var root = document.getElementById('root');
|
|
100
|
-
if (root && root.children.length === 0 && !(root.textContent || '').trim()) {
|
|
101
|
-
send(
|
|
102
|
-
'Runtime error: Preview failed to render; a component or import may have failed silently.',
|
|
103
|
-
'', location.href, undefined, undefined, 'blank-screen'
|
|
104
|
-
);
|
|
105
|
-
} else {
|
|
106
|
-
var images = Array.prototype.slice.call(document.querySelectorAll('img'));
|
|
107
|
-
var controls = Array.prototype.slice.call(
|
|
108
|
-
document.querySelectorAll('button, a[href], input, select, textarea')
|
|
109
|
-
);
|
|
110
|
-
var checks = {
|
|
111
|
-
viewportWidth: document.documentElement.clientWidth || window.innerWidth || 0,
|
|
112
|
-
horizontalOverflow:
|
|
113
|
-
document.documentElement.scrollWidth >
|
|
114
|
-
(document.documentElement.clientWidth || window.innerWidth || 0) + 2,
|
|
115
|
-
brokenImages: images.filter(function(img) {
|
|
116
|
-
return img.complete && img.naturalWidth === 0;
|
|
117
|
-
}).length,
|
|
118
|
-
missingImageAlt: images.filter(function(img) {
|
|
119
|
-
return !img.hasAttribute('alt');
|
|
120
|
-
}).length,
|
|
121
|
-
unlabeledControls: controls.filter(function(control) {
|
|
122
|
-
var label = (
|
|
123
|
-
control.getAttribute('aria-label') ||
|
|
124
|
-
control.getAttribute('title') ||
|
|
125
|
-
control.textContent ||
|
|
126
|
-
control.value ||
|
|
127
|
-
''
|
|
128
|
-
).trim();
|
|
129
|
-
return !label;
|
|
130
|
-
}).length
|
|
131
|
-
};
|
|
132
|
-
window.parent.postMessage({
|
|
133
|
-
source: 'adaptar-preview',
|
|
134
|
-
type: 'preview-stable',
|
|
135
|
-
operationId: operationId,
|
|
136
|
-
stableAt: Date.now(),
|
|
137
|
-
hasRootContent: Boolean(root && (root.children.length || (root.textContent || '').trim())),
|
|
138
|
-
checks: checks
|
|
139
|
-
}, '*');
|
|
140
|
-
}
|
|
141
|
-
}, 3000);
|
|
340
|
+
setTimeout(confirmPreviewHealth, 3000);
|
|
142
341
|
});
|
|
143
342
|
|
|
144
|
-
window.setInterval(function() {
|
|
343
|
+
window.setInterval(function() {
|
|
344
|
+
sendRendered(true);
|
|
345
|
+
}, 20000);
|
|
346
|
+
window.setInterval(function() {
|
|
347
|
+
recoverBlockingErrorsIfRendered('root-health-monitor');
|
|
348
|
+
}, 1000);
|
|
145
349
|
})();`;
|
package/package.json
CHANGED
package/src/hmr-interceptor.ts
CHANGED
|
@@ -1,14 +1,54 @@
|
|
|
1
1
|
import type { ViteDevServer, HMRPayload } from "vite";
|
|
2
2
|
|
|
3
|
-
export interface AdaptarErrorPayload {
|
|
4
|
-
message: string;
|
|
5
|
-
stack: string;
|
|
6
|
-
filename: string | undefined;
|
|
7
|
-
lineno: number | undefined;
|
|
8
|
-
colno: number | undefined;
|
|
9
|
-
source: string;
|
|
10
|
-
plugin: string | undefined;
|
|
11
|
-
|
|
3
|
+
export interface AdaptarErrorPayload {
|
|
4
|
+
message: string;
|
|
5
|
+
stack: string;
|
|
6
|
+
filename: string | undefined;
|
|
7
|
+
lineno: number | undefined;
|
|
8
|
+
colno: number | undefined;
|
|
9
|
+
source: string;
|
|
10
|
+
plugin: string | undefined;
|
|
11
|
+
classification: "render-blocking" | "diagnostic";
|
|
12
|
+
category: "compile" | "resource";
|
|
13
|
+
severity: "error" | "advisory";
|
|
14
|
+
resourceKind:
|
|
15
|
+
| "module"
|
|
16
|
+
| "stylesheet"
|
|
17
|
+
| "image"
|
|
18
|
+
| "font"
|
|
19
|
+
| "media"
|
|
20
|
+
| "unknown";
|
|
21
|
+
renderBlocking: boolean;
|
|
22
|
+
rootHealth: {
|
|
23
|
+
state: "unknown";
|
|
24
|
+
hasRootContent: undefined;
|
|
25
|
+
readyState: undefined;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function classifyViteResource(
|
|
30
|
+
filename: string | undefined,
|
|
31
|
+
plugin: string | undefined,
|
|
32
|
+
): AdaptarErrorPayload["resourceKind"] {
|
|
33
|
+
const signal = `${filename ?? ""} ${plugin ?? ""}`.toLowerCase();
|
|
34
|
+
|
|
35
|
+
if (/\.(?:css|less|sass|scss|styl)(?:$|[?#\s])/.test(signal) || /css|postcss|tailwind/.test(signal)) {
|
|
36
|
+
return "stylesheet";
|
|
37
|
+
}
|
|
38
|
+
if (/\.(?:avif|bmp|gif|ico|jpe?g|png|svg|webp)(?:$|[?#\s])/.test(signal)) {
|
|
39
|
+
return "image";
|
|
40
|
+
}
|
|
41
|
+
if (/\.(?:woff2?|ttf|otf|eot)(?:$|[?#\s])/.test(signal)) {
|
|
42
|
+
return "font";
|
|
43
|
+
}
|
|
44
|
+
if (/\.(?:aac|flac|m4a|m4v|mov|mp3|mp4|ogg|ogv|wav|webm|vtt)(?:$|[?#\s])/.test(signal)) {
|
|
45
|
+
return "media";
|
|
46
|
+
}
|
|
47
|
+
if (/\.(?:c|m)?(?:j|t)sx?(?:$|[?#\s])/.test(signal) || /react|swc|typescript|vite/.test(signal)) {
|
|
48
|
+
return "module";
|
|
49
|
+
}
|
|
50
|
+
return "unknown";
|
|
51
|
+
}
|
|
12
52
|
|
|
13
53
|
// How long (ms) to suppress a full-reload after a fatal error so the
|
|
14
54
|
// error overlay stays visible before the iframe clears it.
|
|
@@ -35,18 +75,34 @@ export function interceptHmrErrors(server: ViteDevServer): void {
|
|
|
35
75
|
(server.ws as any).send = function (payload: HMRPayload): void {
|
|
36
76
|
if (payload && typeof payload === "object") {
|
|
37
77
|
// ── Fatal build / HMR error ──────────────────────────────────────────
|
|
38
|
-
if (payload.type === "error") {
|
|
39
|
-
const { err } = payload;
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
78
|
+
if (payload.type === "error") {
|
|
79
|
+
const { err } = payload;
|
|
80
|
+
const filename = err.id ?? err.loc?.file;
|
|
81
|
+
const resourceKind = classifyViteResource(filename, err.plugin);
|
|
82
|
+
// A Vite failure in application/module compilation invalidates the
|
|
83
|
+
// candidate. Asset and stylesheet failures remain visible diagnostics
|
|
84
|
+
// because they do not prove that the application cannot render.
|
|
85
|
+
const renderBlocking = resourceKind === "module" || resourceKind === "unknown";
|
|
86
|
+
|
|
87
|
+
const formatted: AdaptarErrorPayload = {
|
|
88
|
+
message: `[vite] ${err.message || "Internal server error"}`,
|
|
89
|
+
stack: [err.stack, err.frame].filter(Boolean).join("\n\n"),
|
|
90
|
+
filename,
|
|
91
|
+
lineno: err.loc?.line,
|
|
92
|
+
colno: err.loc?.column,
|
|
93
|
+
source: "vite-hmr",
|
|
94
|
+
plugin: err.plugin,
|
|
95
|
+
classification: renderBlocking ? "render-blocking" : "diagnostic",
|
|
96
|
+
category: renderBlocking ? "compile" : "resource",
|
|
97
|
+
severity: renderBlocking ? "error" : "advisory",
|
|
98
|
+
resourceKind,
|
|
99
|
+
renderBlocking,
|
|
100
|
+
rootHealth: {
|
|
101
|
+
state: "unknown",
|
|
102
|
+
hasRootContent: undefined,
|
|
103
|
+
readyState: undefined,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
50
106
|
|
|
51
107
|
originalSend({
|
|
52
108
|
type: "custom",
|
|
@@ -54,8 +110,12 @@ export function interceptHmrErrors(server: ViteDevServer): void {
|
|
|
54
110
|
data: formatted,
|
|
55
111
|
});
|
|
56
112
|
|
|
57
|
-
//
|
|
58
|
-
|
|
113
|
+
// Only render-blocking compilation errors should interrupt Vite's
|
|
114
|
+
// normal reload path. Diagnostic asset/style failures are reported but
|
|
115
|
+
// are allowed to recover normally.
|
|
116
|
+
if (renderBlocking) {
|
|
117
|
+
suppressReloadUntil = Date.now() + RELOAD_SUPPRESSION_MS;
|
|
118
|
+
}
|
|
59
119
|
originalSend(payload);
|
|
60
120
|
return;
|
|
61
121
|
}
|
package/src/html-tags.ts
CHANGED
|
@@ -8,6 +8,16 @@ import { SELECTION_BRIDGE_SCRIPT } from "./scripts/selection-bridge.js";
|
|
|
8
8
|
*/
|
|
9
9
|
const HMR_LISTENER_SCRIPT = /* js */ `
|
|
10
10
|
const adaptarOperationId = new URL(window.location.href).searchParams.get('__adaptarOperation') || undefined;
|
|
11
|
+
let adaptarBlockingHmrError = false;
|
|
12
|
+
const readAdaptarRootHealth = () => {
|
|
13
|
+
const root = document.getElementById('root');
|
|
14
|
+
const hasRootContent = Boolean(root && (root.children.length || (root.textContent || '').trim()));
|
|
15
|
+
return {
|
|
16
|
+
state: !root ? 'missing' : (hasRootContent ? 'healthy' : 'empty'),
|
|
17
|
+
hasRootContent,
|
|
18
|
+
readyState: document.readyState,
|
|
19
|
+
};
|
|
20
|
+
};
|
|
11
21
|
const postToHost = (type, data) => {
|
|
12
22
|
try {
|
|
13
23
|
window.parent.postMessage({
|
|
@@ -21,14 +31,40 @@ const HMR_LISTENER_SCRIPT = /* js */ `
|
|
|
21
31
|
|
|
22
32
|
if (import.meta.hot) {
|
|
23
33
|
import.meta.hot.on('adaptar:error', (data) => {
|
|
24
|
-
|
|
34
|
+
if (data?.renderBlocking === true) {
|
|
35
|
+
adaptarBlockingHmrError = true;
|
|
36
|
+
}
|
|
37
|
+
postToHost('error', {
|
|
38
|
+
error: {
|
|
39
|
+
...data,
|
|
40
|
+
rootHealth: readAdaptarRootHealth(),
|
|
41
|
+
},
|
|
42
|
+
});
|
|
25
43
|
});
|
|
26
44
|
import.meta.hot.on('adaptar:compile-success', (data) => {
|
|
27
|
-
|
|
45
|
+
const rootHealth = readAdaptarRootHealth();
|
|
46
|
+
postToHost('compile-success', {
|
|
47
|
+
...(data || {}),
|
|
48
|
+
rootHealth,
|
|
49
|
+
});
|
|
50
|
+
if (adaptarBlockingHmrError && rootHealth.hasRootContent) {
|
|
51
|
+
adaptarBlockingHmrError = false;
|
|
52
|
+
postToHost('preview-recovered', {
|
|
53
|
+
recoveredAt: Date.now(),
|
|
54
|
+
reason: 'hmr-compile-recovered',
|
|
55
|
+
resolvedSources: ['vite-hmr'],
|
|
56
|
+
hasRootContent: true,
|
|
57
|
+
rootHealth,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
28
60
|
});
|
|
29
61
|
}
|
|
30
62
|
|
|
31
|
-
postToHost('compile-success', {
|
|
63
|
+
postToHost('compile-success', {
|
|
64
|
+
compiledAt: Date.now(),
|
|
65
|
+
initial: true,
|
|
66
|
+
rootHealth: readAdaptarRootHealth(),
|
|
67
|
+
});
|
|
32
68
|
`.trim();
|
|
33
69
|
|
|
34
70
|
export function buildHtmlTags(): HtmlTagDescriptor[] {
|
|
@@ -1,145 +1,349 @@
|
|
|
1
|
-
export const ERROR_BRIDGE_SCRIPT = /* js */ `(function(){
|
|
1
|
+
export const ERROR_BRIDGE_SCRIPT = /* js */ `(function(){
|
|
2
2
|
if (window.__ADAPTAR_ERROR_BRIDGE__) return;
|
|
3
3
|
window.__ADAPTAR_ERROR_BRIDGE__ = true;
|
|
4
4
|
|
|
5
5
|
var operationId = new URL(window.location.href).searchParams.get('__adaptarOperation') || undefined;
|
|
6
|
+
var activeBlockingSources = Object.create(null);
|
|
7
|
+
var blankCheckStartedAt = 0;
|
|
8
|
+
var blankCheckTimer;
|
|
9
|
+
var blankFailureReported = false;
|
|
10
|
+
var BLANK_CONFIRMATION_MS = 8000;
|
|
11
|
+
var ROOT_CHECK_INTERVAL_MS = 500;
|
|
12
|
+
|
|
13
|
+
function readRootHealth() {
|
|
14
|
+
var root = document.getElementById('root');
|
|
15
|
+
var hasRootContent = Boolean(
|
|
16
|
+
root && (root.children.length || (root.textContent || '').trim())
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
state: !root ? 'missing' : (hasRootContent ? 'healthy' : 'empty'),
|
|
21
|
+
hasRootContent: hasRootContent,
|
|
22
|
+
readyState: document.readyState
|
|
23
|
+
};
|
|
24
|
+
}
|
|
6
25
|
|
|
7
26
|
function sendRendered(heartbeat) {
|
|
8
27
|
try {
|
|
9
|
-
var
|
|
28
|
+
var rootHealth = readRootHealth();
|
|
10
29
|
window.parent.postMessage({
|
|
11
30
|
source: 'adaptar-preview',
|
|
12
31
|
type: heartbeat ? 'render-heartbeat' : 'preview-rendered',
|
|
13
32
|
operationId: operationId,
|
|
14
33
|
renderedAt: Date.now(),
|
|
15
|
-
hasRootContent:
|
|
34
|
+
hasRootContent: rootHealth.hasRootContent,
|
|
35
|
+
rootHealth: rootHealth
|
|
16
36
|
}, '*');
|
|
17
37
|
} catch (_) {}
|
|
18
38
|
}
|
|
19
|
-
|
|
20
|
-
function
|
|
21
|
-
|
|
39
|
+
|
|
40
|
+
function classifyResource(target, url) {
|
|
41
|
+
var tag = String((target && target.tagName) || '').toLowerCase();
|
|
42
|
+
var rel = String((target && target.rel) || '').toLowerCase();
|
|
43
|
+
var as = String((target && target.as) || '').toLowerCase();
|
|
44
|
+
var type = String((target && target.type) || '').toLowerCase();
|
|
45
|
+
var value = String(url || '').split(/[?#]/)[0].toLowerCase();
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
as === 'font' ||
|
|
49
|
+
type.indexOf('font/') === 0 ||
|
|
50
|
+
/\\.(woff2?|ttf|otf|eot)$/.test(value)
|
|
51
|
+
) return 'font';
|
|
52
|
+
if (
|
|
53
|
+
tag === 'img' ||
|
|
54
|
+
as === 'image' ||
|
|
55
|
+
/\\.(avif|bmp|gif|ico|jpe?g|png|svg|webp)$/.test(value)
|
|
56
|
+
) return 'image';
|
|
57
|
+
if (
|
|
58
|
+
tag === 'audio' || tag === 'video' || tag === 'source' || tag === 'track' ||
|
|
59
|
+
as === 'audio' || as === 'video' ||
|
|
60
|
+
/\\.(aac|flac|m4a|m4v|mov|mp3|mp4|ogg|ogv|wav|webm|vtt)$/.test(value)
|
|
61
|
+
) return 'media';
|
|
62
|
+
if (
|
|
63
|
+
tag === 'script' ||
|
|
64
|
+
as === 'script' ||
|
|
65
|
+
rel === 'modulepreload' ||
|
|
66
|
+
type === 'module' ||
|
|
67
|
+
/\\.(c|m)?(j|t)sx?$/.test(value)
|
|
68
|
+
) return 'module';
|
|
69
|
+
if (
|
|
70
|
+
(tag === 'link' && rel === 'stylesheet') ||
|
|
71
|
+
as === 'style' ||
|
|
72
|
+
type === 'text/css' ||
|
|
73
|
+
/\\.(css|less|sass|scss|styl)$/.test(value)
|
|
74
|
+
) return 'stylesheet';
|
|
75
|
+
return 'unknown';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resourceFailureMessage(resourceKind, url) {
|
|
79
|
+
var labels = {
|
|
80
|
+
font: 'Failed to load font: ',
|
|
81
|
+
image: 'Failed to load image: ',
|
|
82
|
+
media: 'Failed to load media: ',
|
|
83
|
+
module: 'Failed to load module: ',
|
|
84
|
+
stylesheet: 'Failed to load stylesheet: ',
|
|
85
|
+
unknown: 'Failed to load resource: '
|
|
86
|
+
};
|
|
87
|
+
return (labels[resourceKind] || labels.unknown) + url;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function send(message, stack, filename, lineno, colno, source, plugin, metadata) {
|
|
91
|
+
try {
|
|
92
|
+
var details = metadata || {};
|
|
93
|
+
var rootHealth = readRootHealth();
|
|
94
|
+
var renderBlocking = details.renderBlocking === true || (
|
|
95
|
+
details.blockWhenRootEmpty === true && !rootHealth.hasRootContent
|
|
96
|
+
);
|
|
97
|
+
var errorSource = source || 'unknown';
|
|
98
|
+
|
|
99
|
+
if (renderBlocking) {
|
|
100
|
+
activeBlockingSources[errorSource] = true;
|
|
101
|
+
}
|
|
102
|
+
|
|
22
103
|
window.parent.postMessage({
|
|
23
104
|
source: 'adaptar-preview',
|
|
24
105
|
type: 'error',
|
|
25
106
|
operationId: operationId,
|
|
26
107
|
error: {
|
|
27
|
-
message: message || 'Unknown error',
|
|
28
|
-
stack: stack || '',
|
|
29
|
-
filename: filename,
|
|
30
|
-
lineno: lineno,
|
|
31
|
-
colno: colno,
|
|
32
|
-
source: source,
|
|
33
|
-
plugin: plugin
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
108
|
+
message: message || 'Unknown error',
|
|
109
|
+
stack: stack || '',
|
|
110
|
+
filename: filename,
|
|
111
|
+
lineno: lineno,
|
|
112
|
+
colno: colno,
|
|
113
|
+
source: source,
|
|
114
|
+
plugin: plugin,
|
|
115
|
+
classification: renderBlocking ? 'render-blocking' : 'diagnostic',
|
|
116
|
+
category: details.category || 'runtime',
|
|
117
|
+
severity: renderBlocking ? 'error' : 'advisory',
|
|
118
|
+
resourceKind: details.resourceKind || 'unknown',
|
|
119
|
+
renderBlocking: renderBlocking,
|
|
120
|
+
rootHealth: rootHealth
|
|
121
|
+
}
|
|
122
|
+
}, '*');
|
|
123
|
+
} catch (_) {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function recoverBlockingErrorsIfRendered(reason) {
|
|
127
|
+
try {
|
|
128
|
+
var rootHealth = readRootHealth();
|
|
129
|
+
var resolvedSources = Object.keys(activeBlockingSources);
|
|
130
|
+
if (!rootHealth.hasRootContent || resolvedSources.length === 0) return;
|
|
131
|
+
|
|
132
|
+
activeBlockingSources = Object.create(null);
|
|
133
|
+
window.parent.postMessage({
|
|
134
|
+
source: 'adaptar-preview',
|
|
135
|
+
type: 'preview-recovered',
|
|
136
|
+
operationId: operationId,
|
|
137
|
+
recoveredAt: Date.now(),
|
|
138
|
+
reason: reason || 'root-healthy',
|
|
139
|
+
resolvedSources: resolvedSources,
|
|
140
|
+
hasRootContent: true,
|
|
141
|
+
rootHealth: rootHealth
|
|
142
|
+
}, '*');
|
|
143
|
+
} catch (_) {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Resource failures remain observable, but only a module required before
|
|
147
|
+
// the application mounts can be classified as render-blocking. Images,
|
|
148
|
+
// fonts, media, and stylesheets are quality diagnostics.
|
|
149
|
+
window.addEventListener('error', function(e) {
|
|
150
|
+
var target = e.target || e.srcElement;
|
|
151
|
+
if (target && target !== window && target.tagName) {
|
|
152
|
+
var url = target.src || target.href || target.currentSrc;
|
|
153
|
+
var resourceKind = classifyResource(target, url);
|
|
154
|
+
if (url || resourceKind !== 'unknown') {
|
|
155
|
+
send(
|
|
156
|
+
resourceFailureMessage(resourceKind, url || '(unknown resource URL)'),
|
|
157
|
+
'', url, undefined, undefined, 'resource-load', undefined,
|
|
158
|
+
{
|
|
159
|
+
category: 'resource',
|
|
160
|
+
resourceKind: resourceKind,
|
|
161
|
+
blockWhenRootEmpty: resourceKind === 'module'
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
send(
|
|
169
|
+
e.message || (e.error && e.error.message) || 'Runtime error occurred',
|
|
170
|
+
e.error ? e.error.stack : '',
|
|
171
|
+
e.filename, e.lineno, e.colno, 'runtime', undefined,
|
|
172
|
+
{ category: 'runtime', resourceKind: 'module', blockWhenRootEmpty: true }
|
|
173
|
+
);
|
|
174
|
+
}, true);
|
|
175
|
+
|
|
176
|
+
// An async failure is blocking only when the application has no rendered
|
|
177
|
+
// root. Interaction failures in an otherwise-rendered page remain visible
|
|
178
|
+
// diagnostics and never invalidate the candidate by themselves.
|
|
179
|
+
window.addEventListener('unhandledrejection', function(e) {
|
|
180
|
+
var r = e.reason;
|
|
181
|
+
send(
|
|
182
|
+
r && r.message ? r.message : String(r || 'Unhandled Promise Rejection'),
|
|
183
|
+
r && r.stack,
|
|
184
|
+
r && r.fileName,
|
|
185
|
+
r && r.lineNumber,
|
|
186
|
+
r && r.columnNumber,
|
|
187
|
+
'unhandledrejection', undefined,
|
|
188
|
+
{ category: 'runtime', resourceKind: 'module', blockWhenRootEmpty: true }
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Intercept only errors that indicate a Vite/module compilation failure.
|
|
193
|
+
// Their render-blocking status is still tied to an empty application root;
|
|
194
|
+
// Vite HMR compile failures are classified separately by the server bridge.
|
|
195
|
+
var nativeConsoleError = console.error;
|
|
196
|
+
console.error = function() {
|
|
197
|
+
var args = Array.prototype.slice.call(arguments);
|
|
198
|
+
var msg = args.map(function(a) {
|
|
199
|
+
return typeof a === 'string' ? a : (a && a.message ? a.message : String(a));
|
|
200
|
+
}).join(' ');
|
|
201
|
+
if (/Failed to load module|Internal Server Error|Syntax Error/i.test(msg)) {
|
|
202
|
+
send(
|
|
203
|
+
msg, '', undefined, undefined, undefined, 'console.error', undefined,
|
|
204
|
+
{ category: 'compile', resourceKind: 'module', blockWhenRootEmpty: true }
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return nativeConsoleError.apply(console, args);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
function sendStable(rootHealth) {
|
|
211
|
+
var images = Array.prototype.slice.call(document.querySelectorAll('img'));
|
|
212
|
+
var controls = Array.prototype.slice.call(
|
|
213
|
+
document.querySelectorAll('button, a[href], input, select, textarea')
|
|
214
|
+
);
|
|
215
|
+
var checks = {
|
|
216
|
+
viewportWidth: document.documentElement.clientWidth || window.innerWidth || 0,
|
|
217
|
+
horizontalOverflow:
|
|
218
|
+
document.documentElement.scrollWidth >
|
|
219
|
+
(document.documentElement.clientWidth || window.innerWidth || 0) + 2,
|
|
220
|
+
brokenImages: images.filter(function(img) {
|
|
221
|
+
return img.complete && img.naturalWidth === 0;
|
|
222
|
+
}).length,
|
|
223
|
+
missingImageAlt: images.filter(function(img) {
|
|
224
|
+
return !img.hasAttribute('alt');
|
|
225
|
+
}).length,
|
|
226
|
+
unlabeledControls: controls.filter(function(control) {
|
|
227
|
+
var label = (
|
|
228
|
+
control.getAttribute('aria-label') ||
|
|
229
|
+
control.getAttribute('title') ||
|
|
230
|
+
control.textContent ||
|
|
231
|
+
control.value ||
|
|
232
|
+
''
|
|
233
|
+
).trim();
|
|
234
|
+
return !label;
|
|
235
|
+
}).length
|
|
236
|
+
};
|
|
237
|
+
var diagnostics = [];
|
|
238
|
+
|
|
239
|
+
if (checks.horizontalOverflow) {
|
|
240
|
+
diagnostics.push({
|
|
241
|
+
code: 'horizontal-overflow',
|
|
242
|
+
classification: 'diagnostic',
|
|
243
|
+
category: 'layout',
|
|
244
|
+
severity: 'advisory',
|
|
245
|
+
resourceKind: 'quality',
|
|
246
|
+
renderBlocking: false,
|
|
247
|
+
message: 'The preview has horizontal overflow.'
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (checks.brokenImages > 0) {
|
|
251
|
+
diagnostics.push({
|
|
252
|
+
code: 'broken-images',
|
|
253
|
+
classification: 'diagnostic',
|
|
254
|
+
category: 'asset',
|
|
255
|
+
severity: 'advisory',
|
|
256
|
+
resourceKind: 'image',
|
|
257
|
+
renderBlocking: false,
|
|
258
|
+
message: 'One or more images could not be displayed.',
|
|
259
|
+
count: checks.brokenImages
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
if (checks.missingImageAlt > 0) {
|
|
263
|
+
diagnostics.push({
|
|
264
|
+
code: 'missing-image-alt',
|
|
265
|
+
classification: 'diagnostic',
|
|
266
|
+
category: 'accessibility',
|
|
267
|
+
severity: 'advisory',
|
|
268
|
+
resourceKind: 'image',
|
|
269
|
+
renderBlocking: false,
|
|
270
|
+
message: 'One or more images are missing alternative text.',
|
|
271
|
+
count: checks.missingImageAlt
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (checks.unlabeledControls > 0) {
|
|
275
|
+
diagnostics.push({
|
|
276
|
+
code: 'unlabeled-controls',
|
|
277
|
+
classification: 'diagnostic',
|
|
278
|
+
category: 'accessibility',
|
|
279
|
+
severity: 'advisory',
|
|
280
|
+
resourceKind: 'control',
|
|
281
|
+
renderBlocking: false,
|
|
282
|
+
message: 'One or more controls do not have an accessible label.',
|
|
283
|
+
count: checks.unlabeledControls
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
window.parent.postMessage({
|
|
288
|
+
source: 'adaptar-preview',
|
|
289
|
+
type: 'preview-stable',
|
|
290
|
+
operationId: operationId,
|
|
291
|
+
stableAt: Date.now(),
|
|
292
|
+
hasRootContent: rootHealth.hasRootContent,
|
|
293
|
+
rootHealth: rootHealth,
|
|
294
|
+
checks: checks,
|
|
295
|
+
diagnostics: diagnostics
|
|
296
|
+
}, '*');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function confirmPreviewHealth() {
|
|
300
|
+
var rootHealth = readRootHealth();
|
|
301
|
+
if (rootHealth.hasRootContent) {
|
|
302
|
+
if (blankCheckTimer) {
|
|
303
|
+
clearTimeout(blankCheckTimer);
|
|
304
|
+
blankCheckTimer = undefined;
|
|
305
|
+
}
|
|
306
|
+
blankCheckStartedAt = 0;
|
|
307
|
+
blankFailureReported = false;
|
|
308
|
+
recoverBlockingErrorsIfRendered('root-recovered');
|
|
309
|
+
sendStable(rootHealth);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!blankCheckStartedAt) blankCheckStartedAt = Date.now();
|
|
314
|
+
if (Date.now() - blankCheckStartedAt >= BLANK_CONFIRMATION_MS) {
|
|
315
|
+
if (!blankFailureReported) {
|
|
316
|
+
blankFailureReported = true;
|
|
46
317
|
send(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
? 'Failed to load image: '
|
|
51
|
-
: 'Failed to load module: ') + url,
|
|
52
|
-
'', url, undefined, undefined, 'resource-load'
|
|
318
|
+
'Runtime error: Preview failed to render; a component or import may have failed silently.',
|
|
319
|
+
'', location.href, undefined, undefined, 'blank-screen', undefined,
|
|
320
|
+
{ category: 'render', resourceKind: 'document', renderBlocking: true }
|
|
53
321
|
);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// Unhandled promise rejections
|
|
65
|
-
window.addEventListener('unhandledrejection', function(e) {
|
|
66
|
-
var r = e.reason;
|
|
67
|
-
send(
|
|
68
|
-
r && r.message ? r.message : String(r || 'Unhandled Promise Rejection'),
|
|
69
|
-
r && r.stack,
|
|
70
|
-
r && r.fileName,
|
|
71
|
-
r && r.lineNumber,
|
|
72
|
-
r && r.columnNumber,
|
|
73
|
-
'unhandledrejection'
|
|
74
|
-
);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Console error interception for silent Vite/Syntax errors
|
|
78
|
-
var nativeConsoleError = console.error;
|
|
79
|
-
console.error = function() {
|
|
80
|
-
var args = Array.prototype.slice.call(arguments);
|
|
81
|
-
var msg = args.map(function(a) {
|
|
82
|
-
return typeof a === 'string' ? a : (a && a.message ? a.message : String(a));
|
|
83
|
-
}).join(' ');
|
|
84
|
-
if (/Failed to load module|Internal Server Error|Syntax Error/i.test(msg)) {
|
|
85
|
-
send(msg, '', undefined, undefined, undefined, 'console.error');
|
|
86
|
-
}
|
|
87
|
-
return nativeConsoleError.apply(console, args);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// Blank screen detector
|
|
322
|
+
}
|
|
323
|
+
blankCheckTimer = setTimeout(confirmPreviewHealth, ROOT_CHECK_INTERVAL_MS);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
blankCheckTimer = setTimeout(confirmPreviewHealth, ROOT_CHECK_INTERVAL_MS);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// A blank root is blocking only when it remains blank across a sustained
|
|
331
|
+
// observation window. Normal React mounting and Vite reloads are transient.
|
|
91
332
|
window.addEventListener('load', function() {
|
|
333
|
+
blankCheckStartedAt = Date.now();
|
|
92
334
|
requestAnimationFrame(function() {
|
|
93
335
|
requestAnimationFrame(function() {
|
|
94
336
|
sendRendered(false);
|
|
95
337
|
});
|
|
96
338
|
});
|
|
97
339
|
|
|
98
|
-
setTimeout(
|
|
99
|
-
var root = document.getElementById('root');
|
|
100
|
-
if (root && root.children.length === 0 && !(root.textContent || '').trim()) {
|
|
101
|
-
send(
|
|
102
|
-
'Runtime error: Preview failed to render; a component or import may have failed silently.',
|
|
103
|
-
'', location.href, undefined, undefined, 'blank-screen'
|
|
104
|
-
);
|
|
105
|
-
} else {
|
|
106
|
-
var images = Array.prototype.slice.call(document.querySelectorAll('img'));
|
|
107
|
-
var controls = Array.prototype.slice.call(
|
|
108
|
-
document.querySelectorAll('button, a[href], input, select, textarea')
|
|
109
|
-
);
|
|
110
|
-
var checks = {
|
|
111
|
-
viewportWidth: document.documentElement.clientWidth || window.innerWidth || 0,
|
|
112
|
-
horizontalOverflow:
|
|
113
|
-
document.documentElement.scrollWidth >
|
|
114
|
-
(document.documentElement.clientWidth || window.innerWidth || 0) + 2,
|
|
115
|
-
brokenImages: images.filter(function(img) {
|
|
116
|
-
return img.complete && img.naturalWidth === 0;
|
|
117
|
-
}).length,
|
|
118
|
-
missingImageAlt: images.filter(function(img) {
|
|
119
|
-
return !img.hasAttribute('alt');
|
|
120
|
-
}).length,
|
|
121
|
-
unlabeledControls: controls.filter(function(control) {
|
|
122
|
-
var label = (
|
|
123
|
-
control.getAttribute('aria-label') ||
|
|
124
|
-
control.getAttribute('title') ||
|
|
125
|
-
control.textContent ||
|
|
126
|
-
control.value ||
|
|
127
|
-
''
|
|
128
|
-
).trim();
|
|
129
|
-
return !label;
|
|
130
|
-
}).length
|
|
131
|
-
};
|
|
132
|
-
window.parent.postMessage({
|
|
133
|
-
source: 'adaptar-preview',
|
|
134
|
-
type: 'preview-stable',
|
|
135
|
-
operationId: operationId,
|
|
136
|
-
stableAt: Date.now(),
|
|
137
|
-
hasRootContent: Boolean(root && (root.children.length || (root.textContent || '').trim())),
|
|
138
|
-
checks: checks
|
|
139
|
-
}, '*');
|
|
140
|
-
}
|
|
141
|
-
}, 3000);
|
|
340
|
+
setTimeout(confirmPreviewHealth, 3000);
|
|
142
341
|
});
|
|
143
342
|
|
|
144
|
-
window.setInterval(function() {
|
|
343
|
+
window.setInterval(function() {
|
|
344
|
+
sendRendered(true);
|
|
345
|
+
}, 20000);
|
|
346
|
+
window.setInterval(function() {
|
|
347
|
+
recoverBlockingErrorsIfRendered('root-health-monitor');
|
|
348
|
+
}, 1000);
|
|
145
349
|
})();`;
|