adaptar-vite-plugin 1.0.5 → 1.0.7

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/html-tags.js CHANGED
@@ -6,6 +6,7 @@ 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;
9
10
  const readAdaptarRootHealth = () => {
10
11
  const root = document.getElementById('root');
11
12
  const hasRootContent = Boolean(root && (root.children.length || (root.textContent || '').trim()));
@@ -28,6 +29,9 @@ const HMR_LISTENER_SCRIPT = /* js */ `
28
29
 
29
30
  if (import.meta.hot) {
30
31
  import.meta.hot.on('adaptar:error', (data) => {
32
+ if (data?.renderBlocking === true) {
33
+ adaptarBlockingHmrError = true;
34
+ }
31
35
  postToHost('error', {
32
36
  error: {
33
37
  ...data,
@@ -36,10 +40,21 @@ const HMR_LISTENER_SCRIPT = /* js */ `
36
40
  });
37
41
  });
38
42
  import.meta.hot.on('adaptar:compile-success', (data) => {
43
+ const rootHealth = readAdaptarRootHealth();
39
44
  postToHost('compile-success', {
40
45
  ...(data || {}),
41
- rootHealth: readAdaptarRootHealth(),
46
+ rootHealth,
42
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
+ }
43
58
  });
44
59
  }
45
60
 
@@ -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 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\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 // 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 // Blank root is a deterministic rendering failure. Quality checks are sent\n // separately as advisory diagnostics once a root is healthy.\n window.addEventListener('load', function() {\n requestAnimationFrame(function() {\n requestAnimationFrame(function() {\n sendRendered(false);\n });\n });\n\n setTimeout(function() {\n var rootHealth = readRootHealth();\n if (!rootHealth.hasRootContent) {\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 } 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 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 }, 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})();";
@@ -3,6 +3,12 @@ export const ERROR_BRIDGE_SCRIPT = /* js */ `(function(){
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;
6
12
 
7
13
  function readRootHealth() {
8
14
  var root = document.getElementById('root');
@@ -88,6 +94,11 @@ export const ERROR_BRIDGE_SCRIPT = /* js */ `(function(){
88
94
  var renderBlocking = details.renderBlocking === true || (
89
95
  details.blockWhenRootEmpty === true && !rootHealth.hasRootContent
90
96
  );
97
+ var errorSource = source || 'unknown';
98
+
99
+ if (renderBlocking) {
100
+ activeBlockingSources[errorSource] = true;
101
+ }
91
102
 
92
103
  window.parent.postMessage({
93
104
  source: 'adaptar-preview',
@@ -112,6 +123,26 @@ export const ERROR_BRIDGE_SCRIPT = /* js */ `(function(){
112
123
  } catch (_) {}
113
124
  }
114
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
+
115
146
  // Resource failures remain observable, but only a module required before
116
147
  // the application mounts can be classified as render-blocking. Images,
117
148
  // fonts, media, and stylesheets are quality diagnostics.
@@ -176,113 +207,143 @@ export const ERROR_BRIDGE_SCRIPT = /* js */ `(function(){
176
207
  return nativeConsoleError.apply(console, args);
177
208
  };
178
209
 
179
- // Blank root is a deterministic rendering failure. Quality checks are sent
180
- // separately as advisory diagnostics once a root is healthy.
181
- window.addEventListener('load', function() {
182
- requestAnimationFrame(function() {
183
- requestAnimationFrame(function() {
184
- sendRendered(false);
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.'
185
248
  });
186
- });
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
+ }
187
286
 
188
- setTimeout(function() {
189
- var rootHealth = readRootHealth();
190
- if (!rootHealth.hasRootContent) {
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;
191
317
  send(
192
318
  'Runtime error: Preview failed to render; a component or import may have failed silently.',
193
319
  '', location.href, undefined, undefined, 'blank-screen', undefined,
194
320
  { category: 'render', resourceKind: 'document', renderBlocking: true }
195
321
  );
196
- } else {
197
- var images = Array.prototype.slice.call(document.querySelectorAll('img'));
198
- var controls = Array.prototype.slice.call(
199
- document.querySelectorAll('button, a[href], input, select, textarea')
200
- );
201
- var checks = {
202
- viewportWidth: document.documentElement.clientWidth || window.innerWidth || 0,
203
- horizontalOverflow:
204
- document.documentElement.scrollWidth >
205
- (document.documentElement.clientWidth || window.innerWidth || 0) + 2,
206
- brokenImages: images.filter(function(img) {
207
- return img.complete && img.naturalWidth === 0;
208
- }).length,
209
- missingImageAlt: images.filter(function(img) {
210
- return !img.hasAttribute('alt');
211
- }).length,
212
- unlabeledControls: controls.filter(function(control) {
213
- var label = (
214
- control.getAttribute('aria-label') ||
215
- control.getAttribute('title') ||
216
- control.textContent ||
217
- control.value ||
218
- ''
219
- ).trim();
220
- return !label;
221
- }).length
222
- };
223
- var diagnostics = [];
322
+ }
323
+ blankCheckTimer = setTimeout(confirmPreviewHealth, ROOT_CHECK_INTERVAL_MS);
324
+ return;
325
+ }
224
326
 
225
- if (checks.horizontalOverflow) {
226
- diagnostics.push({
227
- code: 'horizontal-overflow',
228
- classification: 'diagnostic',
229
- category: 'layout',
230
- severity: 'advisory',
231
- resourceKind: 'quality',
232
- renderBlocking: false,
233
- message: 'The preview has horizontal overflow.'
234
- });
235
- }
236
- if (checks.brokenImages > 0) {
237
- diagnostics.push({
238
- code: 'broken-images',
239
- classification: 'diagnostic',
240
- category: 'asset',
241
- severity: 'advisory',
242
- resourceKind: 'image',
243
- renderBlocking: false,
244
- message: 'One or more images could not be displayed.',
245
- count: checks.brokenImages
246
- });
247
- }
248
- if (checks.missingImageAlt > 0) {
249
- diagnostics.push({
250
- code: 'missing-image-alt',
251
- classification: 'diagnostic',
252
- category: 'accessibility',
253
- severity: 'advisory',
254
- resourceKind: 'image',
255
- renderBlocking: false,
256
- message: 'One or more images are missing alternative text.',
257
- count: checks.missingImageAlt
258
- });
259
- }
260
- if (checks.unlabeledControls > 0) {
261
- diagnostics.push({
262
- code: 'unlabeled-controls',
263
- classification: 'diagnostic',
264
- category: 'accessibility',
265
- severity: 'advisory',
266
- resourceKind: 'control',
267
- renderBlocking: false,
268
- message: 'One or more controls do not have an accessible label.',
269
- count: checks.unlabeledControls
270
- });
271
- }
327
+ blankCheckTimer = setTimeout(confirmPreviewHealth, ROOT_CHECK_INTERVAL_MS);
328
+ }
272
329
 
273
- window.parent.postMessage({
274
- source: 'adaptar-preview',
275
- type: 'preview-stable',
276
- operationId: operationId,
277
- stableAt: Date.now(),
278
- hasRootContent: rootHealth.hasRootContent,
279
- rootHealth: rootHealth,
280
- checks: checks,
281
- diagnostics: diagnostics
282
- }, '*');
283
- }
284
- }, 3000);
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.
332
+ window.addEventListener('load', function() {
333
+ blankCheckStartedAt = Date.now();
334
+ requestAnimationFrame(function() {
335
+ requestAnimationFrame(function() {
336
+ sendRendered(false);
337
+ });
338
+ });
339
+
340
+ setTimeout(confirmPreviewHealth, 3000);
285
341
  });
286
342
 
287
- window.setInterval(function() { sendRendered(true); }, 20000);
343
+ window.setInterval(function() {
344
+ sendRendered(true);
345
+ }, 20000);
346
+ window.setInterval(function() {
347
+ recoverBlockingErrorsIfRendered('root-health-monitor');
348
+ }, 1000);
288
349
  })();`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adaptar-vite-plugin",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Vite plugin for Adaptar preview error and selection bridging",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/html-tags.ts CHANGED
@@ -8,6 +8,7 @@ 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;
11
12
  const readAdaptarRootHealth = () => {
12
13
  const root = document.getElementById('root');
13
14
  const hasRootContent = Boolean(root && (root.children.length || (root.textContent || '').trim()));
@@ -30,6 +31,9 @@ const HMR_LISTENER_SCRIPT = /* js */ `
30
31
 
31
32
  if (import.meta.hot) {
32
33
  import.meta.hot.on('adaptar:error', (data) => {
34
+ if (data?.renderBlocking === true) {
35
+ adaptarBlockingHmrError = true;
36
+ }
33
37
  postToHost('error', {
34
38
  error: {
35
39
  ...data,
@@ -38,10 +42,21 @@ const HMR_LISTENER_SCRIPT = /* js */ `
38
42
  });
39
43
  });
40
44
  import.meta.hot.on('adaptar:compile-success', (data) => {
45
+ const rootHealth = readAdaptarRootHealth();
41
46
  postToHost('compile-success', {
42
47
  ...(data || {}),
43
- rootHealth: readAdaptarRootHealth(),
48
+ rootHealth,
44
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
+ }
45
60
  });
46
61
  }
47
62
 
@@ -3,6 +3,12 @@ export const ERROR_BRIDGE_SCRIPT = /* js */ `(function(){
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;
6
12
 
7
13
  function readRootHealth() {
8
14
  var root = document.getElementById('root');
@@ -88,6 +94,11 @@ export const ERROR_BRIDGE_SCRIPT = /* js */ `(function(){
88
94
  var renderBlocking = details.renderBlocking === true || (
89
95
  details.blockWhenRootEmpty === true && !rootHealth.hasRootContent
90
96
  );
97
+ var errorSource = source || 'unknown';
98
+
99
+ if (renderBlocking) {
100
+ activeBlockingSources[errorSource] = true;
101
+ }
91
102
 
92
103
  window.parent.postMessage({
93
104
  source: 'adaptar-preview',
@@ -112,6 +123,26 @@ export const ERROR_BRIDGE_SCRIPT = /* js */ `(function(){
112
123
  } catch (_) {}
113
124
  }
114
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
+
115
146
  // Resource failures remain observable, but only a module required before
116
147
  // the application mounts can be classified as render-blocking. Images,
117
148
  // fonts, media, and stylesheets are quality diagnostics.
@@ -176,113 +207,143 @@ export const ERROR_BRIDGE_SCRIPT = /* js */ `(function(){
176
207
  return nativeConsoleError.apply(console, args);
177
208
  };
178
209
 
179
- // Blank root is a deterministic rendering failure. Quality checks are sent
180
- // separately as advisory diagnostics once a root is healthy.
181
- window.addEventListener('load', function() {
182
- requestAnimationFrame(function() {
183
- requestAnimationFrame(function() {
184
- sendRendered(false);
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.'
185
248
  });
186
- });
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
+ }
187
286
 
188
- setTimeout(function() {
189
- var rootHealth = readRootHealth();
190
- if (!rootHealth.hasRootContent) {
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;
191
317
  send(
192
318
  'Runtime error: Preview failed to render; a component or import may have failed silently.',
193
319
  '', location.href, undefined, undefined, 'blank-screen', undefined,
194
320
  { category: 'render', resourceKind: 'document', renderBlocking: true }
195
321
  );
196
- } else {
197
- var images = Array.prototype.slice.call(document.querySelectorAll('img'));
198
- var controls = Array.prototype.slice.call(
199
- document.querySelectorAll('button, a[href], input, select, textarea')
200
- );
201
- var checks = {
202
- viewportWidth: document.documentElement.clientWidth || window.innerWidth || 0,
203
- horizontalOverflow:
204
- document.documentElement.scrollWidth >
205
- (document.documentElement.clientWidth || window.innerWidth || 0) + 2,
206
- brokenImages: images.filter(function(img) {
207
- return img.complete && img.naturalWidth === 0;
208
- }).length,
209
- missingImageAlt: images.filter(function(img) {
210
- return !img.hasAttribute('alt');
211
- }).length,
212
- unlabeledControls: controls.filter(function(control) {
213
- var label = (
214
- control.getAttribute('aria-label') ||
215
- control.getAttribute('title') ||
216
- control.textContent ||
217
- control.value ||
218
- ''
219
- ).trim();
220
- return !label;
221
- }).length
222
- };
223
- var diagnostics = [];
322
+ }
323
+ blankCheckTimer = setTimeout(confirmPreviewHealth, ROOT_CHECK_INTERVAL_MS);
324
+ return;
325
+ }
224
326
 
225
- if (checks.horizontalOverflow) {
226
- diagnostics.push({
227
- code: 'horizontal-overflow',
228
- classification: 'diagnostic',
229
- category: 'layout',
230
- severity: 'advisory',
231
- resourceKind: 'quality',
232
- renderBlocking: false,
233
- message: 'The preview has horizontal overflow.'
234
- });
235
- }
236
- if (checks.brokenImages > 0) {
237
- diagnostics.push({
238
- code: 'broken-images',
239
- classification: 'diagnostic',
240
- category: 'asset',
241
- severity: 'advisory',
242
- resourceKind: 'image',
243
- renderBlocking: false,
244
- message: 'One or more images could not be displayed.',
245
- count: checks.brokenImages
246
- });
247
- }
248
- if (checks.missingImageAlt > 0) {
249
- diagnostics.push({
250
- code: 'missing-image-alt',
251
- classification: 'diagnostic',
252
- category: 'accessibility',
253
- severity: 'advisory',
254
- resourceKind: 'image',
255
- renderBlocking: false,
256
- message: 'One or more images are missing alternative text.',
257
- count: checks.missingImageAlt
258
- });
259
- }
260
- if (checks.unlabeledControls > 0) {
261
- diagnostics.push({
262
- code: 'unlabeled-controls',
263
- classification: 'diagnostic',
264
- category: 'accessibility',
265
- severity: 'advisory',
266
- resourceKind: 'control',
267
- renderBlocking: false,
268
- message: 'One or more controls do not have an accessible label.',
269
- count: checks.unlabeledControls
270
- });
271
- }
327
+ blankCheckTimer = setTimeout(confirmPreviewHealth, ROOT_CHECK_INTERVAL_MS);
328
+ }
272
329
 
273
- window.parent.postMessage({
274
- source: 'adaptar-preview',
275
- type: 'preview-stable',
276
- operationId: operationId,
277
- stableAt: Date.now(),
278
- hasRootContent: rootHealth.hasRootContent,
279
- rootHealth: rootHealth,
280
- checks: checks,
281
- diagnostics: diagnostics
282
- }, '*');
283
- }
284
- }, 3000);
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.
332
+ window.addEventListener('load', function() {
333
+ blankCheckStartedAt = Date.now();
334
+ requestAnimationFrame(function() {
335
+ requestAnimationFrame(function() {
336
+ sendRendered(false);
337
+ });
338
+ });
339
+
340
+ setTimeout(confirmPreviewHealth, 3000);
285
341
  });
286
342
 
287
- window.setInterval(function() { sendRendered(true); }, 20000);
343
+ window.setInterval(function() {
344
+ sendRendered(true);
345
+ }, 20000);
346
+ window.setInterval(function() {
347
+ recoverBlockingErrorsIfRendered('root-health-monitor');
348
+ }, 1000);
288
349
  })();`;