adaptar-vite-plugin 1.0.0
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 +19 -0
- package/dist/hmr-interceptor.js +52 -0
- package/dist/html-tags.d.ts +2 -0
- package/dist/html-tags.js +36 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +36 -0
- package/dist/scripts/error-bridge.d.ts +1 -0
- package/dist/scripts/error-bridge.js +82 -0
- package/dist/scripts/selection-bridge.d.ts +1 -0
- package/dist/scripts/selection-bridge.js +246 -0
- package/package.json +23 -0
- package/src/hmr-interceptor.ts +72 -0
- package/src/html-tags.ts +39 -0
- package/src/index.ts +41 -0
- package/src/scripts/error-bridge.ts +82 -0
- package/src/scripts/selection-bridge.ts +246 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ViteDevServer } from "vite";
|
|
2
|
+
export interface AdaptarErrorPayload {
|
|
3
|
+
message: string;
|
|
4
|
+
stack: string;
|
|
5
|
+
filename: string | undefined;
|
|
6
|
+
lineno: number | undefined;
|
|
7
|
+
colno: number | undefined;
|
|
8
|
+
source: string;
|
|
9
|
+
plugin: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Intercepts Vite's internal WebSocket `send` to catch build/HMR errors
|
|
13
|
+
* at the source and forward them as structured `adaptar:error` custom events.
|
|
14
|
+
*
|
|
15
|
+
* Also suppresses the `full-reload` message that Vite fires immediately after
|
|
16
|
+
* a fatal error — without this the iframe reloads and clears the error overlay
|
|
17
|
+
* before the user has a chance to act on it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function interceptHmrErrors(server: ViteDevServer): void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// How long (ms) to suppress a full-reload after a fatal error so the
|
|
2
|
+
// error overlay stays visible before the iframe clears it.
|
|
3
|
+
const RELOAD_SUPPRESSION_MS = 5_000;
|
|
4
|
+
/**
|
|
5
|
+
* Intercepts Vite's internal WebSocket `send` to catch build/HMR errors
|
|
6
|
+
* at the source and forward them as structured `adaptar:error` custom events.
|
|
7
|
+
*
|
|
8
|
+
* Also suppresses the `full-reload` message that Vite fires immediately after
|
|
9
|
+
* a fatal error — without this the iframe reloads and clears the error overlay
|
|
10
|
+
* before the user has a chance to act on it.
|
|
11
|
+
*/
|
|
12
|
+
export function interceptHmrErrors(server) {
|
|
13
|
+
const originalSend = server.ws.send.bind(server.ws);
|
|
14
|
+
// Tracks whether a fatal error was recently sent so we can suppress the
|
|
15
|
+
// follow-up full-reload for a short window.
|
|
16
|
+
let suppressReloadUntil = 0;
|
|
17
|
+
// Cast to any to override — Vite 7 exposes an overloaded signature that
|
|
18
|
+
// TypeScript cannot directly assign to. We restore full type safety inside
|
|
19
|
+
// the function body by narrowing against HotPayload ourselves.
|
|
20
|
+
server.ws.send = function (payload) {
|
|
21
|
+
if (payload && typeof payload === "object") {
|
|
22
|
+
// ── Fatal build / HMR error ──────────────────────────────────────────
|
|
23
|
+
if (payload.type === "error") {
|
|
24
|
+
const { err } = payload;
|
|
25
|
+
const formatted = {
|
|
26
|
+
message: `[vite] ${err.message || "Internal server error"}`,
|
|
27
|
+
stack: [err.stack, err.frame].filter(Boolean).join("\n\n"),
|
|
28
|
+
filename: err.id ?? err.loc?.file,
|
|
29
|
+
lineno: err.loc?.line,
|
|
30
|
+
colno: err.loc?.column,
|
|
31
|
+
source: "vite-hmr",
|
|
32
|
+
plugin: err.plugin,
|
|
33
|
+
};
|
|
34
|
+
originalSend({
|
|
35
|
+
type: "custom",
|
|
36
|
+
event: "adaptar:error",
|
|
37
|
+
data: formatted,
|
|
38
|
+
});
|
|
39
|
+
// Start suppression window so the follow-up full-reload is blocked.
|
|
40
|
+
suppressReloadUntil = Date.now() + RELOAD_SUPPRESSION_MS;
|
|
41
|
+
originalSend(payload);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// ── Suppress full-reload that follows a fatal error ──────────────────
|
|
45
|
+
if (payload.type === "full-reload" && Date.now() < suppressReloadUntil) {
|
|
46
|
+
suppressReloadUntil = 0; // reset — only suppress once
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
originalSend(payload);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ERROR_BRIDGE_SCRIPT } from "./scripts/error-bridge.js";
|
|
2
|
+
import { SELECTION_BRIDGE_SCRIPT } from "./scripts/selection-bridge.js";
|
|
3
|
+
/**
|
|
4
|
+
* The inline module that subscribes to the `adaptar:error` custom HMR event
|
|
5
|
+
* and forwards it to the parent window via postMessage.
|
|
6
|
+
*/
|
|
7
|
+
const HMR_LISTENER_SCRIPT = /* js */ `
|
|
8
|
+
if (import.meta.hot) {
|
|
9
|
+
import.meta.hot.on('adaptar:error', (data) => {
|
|
10
|
+
try {
|
|
11
|
+
window.parent.postMessage({ source: 'adaptar-preview', type: 'error', error: data }, '*');
|
|
12
|
+
} catch (_) {}
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
`.trim();
|
|
16
|
+
export function buildHtmlTags() {
|
|
17
|
+
return [
|
|
18
|
+
{
|
|
19
|
+
tag: "script",
|
|
20
|
+
injectTo: "head-prepend",
|
|
21
|
+
attrs: { "data-adaptar": "error-bridge" },
|
|
22
|
+
children: ERROR_BRIDGE_SCRIPT,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
tag: "script",
|
|
26
|
+
injectTo: "head-prepend",
|
|
27
|
+
attrs: { "data-adaptar": "selection-bridge" },
|
|
28
|
+
children: SELECTION_BRIDGE_SCRIPT,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
tag: "script",
|
|
32
|
+
attrs: { type: "module" },
|
|
33
|
+
children: HMR_LISTENER_SCRIPT,
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
/**
|
|
3
|
+
* Adaptar Vite Plugin
|
|
4
|
+
*
|
|
5
|
+
* Provides three layers of error and selection bridging between the
|
|
6
|
+
* sandboxed preview iframe and the Adaptar host editor:
|
|
7
|
+
*
|
|
8
|
+
* 1. `error-bridge` — catches runtime JS errors, resource failures,
|
|
9
|
+
* unhandled rejections, and blank-screen scenarios.
|
|
10
|
+
* 2. `selection-bridge` — powers element inspect / edit mode in the preview.
|
|
11
|
+
* 3. `hmr-interceptor` — intercepts Vite's internal WebSocket to surface
|
|
12
|
+
* build/HMR errors as structured `adaptar:error` events.
|
|
13
|
+
*/
|
|
14
|
+
export declare function adaptar(): Plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { interceptHmrErrors } from "./hmr-interceptor.js";
|
|
2
|
+
import { buildHtmlTags } from "./html-tags.js";
|
|
3
|
+
/**
|
|
4
|
+
* Adaptar Vite Plugin
|
|
5
|
+
*
|
|
6
|
+
* Provides three layers of error and selection bridging between the
|
|
7
|
+
* sandboxed preview iframe and the Adaptar host editor:
|
|
8
|
+
*
|
|
9
|
+
* 1. `error-bridge` — catches runtime JS errors, resource failures,
|
|
10
|
+
* unhandled rejections, and blank-screen scenarios.
|
|
11
|
+
* 2. `selection-bridge` — powers element inspect / edit mode in the preview.
|
|
12
|
+
* 3. `hmr-interceptor` — intercepts Vite's internal WebSocket to surface
|
|
13
|
+
* build/HMR errors as structured `adaptar:error` events.
|
|
14
|
+
*/
|
|
15
|
+
export function adaptar() {
|
|
16
|
+
return {
|
|
17
|
+
name: "adaptar-vite-bridge",
|
|
18
|
+
enforce: "pre",
|
|
19
|
+
config() {
|
|
20
|
+
return {
|
|
21
|
+
server: {
|
|
22
|
+
hmr: {
|
|
23
|
+
// Disable Vite's default error overlay — Adaptar renders its own.
|
|
24
|
+
overlay: false,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
transformIndexHtml() {
|
|
30
|
+
return buildHtmlTags();
|
|
31
|
+
},
|
|
32
|
+
configureServer(server) {
|
|
33
|
+
interceptHmrErrors(server);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const ERROR_BRIDGE_SCRIPT = "(function(){\n if (window.__ADAPTAR_ERROR_BRIDGE__) return;\n window.__ADAPTAR_ERROR_BRIDGE__ = true;\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 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')) {\n send(\n (tag === 'link' ? 'Failed to load stylesheet: ' : '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 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 }\n }, 3000);\n });\n})();";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export const ERROR_BRIDGE_SCRIPT = /* js */ `(function(){
|
|
2
|
+
if (window.__ADAPTAR_ERROR_BRIDGE__) return;
|
|
3
|
+
window.__ADAPTAR_ERROR_BRIDGE__ = true;
|
|
4
|
+
|
|
5
|
+
function send(message, stack, filename, lineno, colno, source, plugin) {
|
|
6
|
+
try {
|
|
7
|
+
window.parent.postMessage({
|
|
8
|
+
source: 'adaptar-preview',
|
|
9
|
+
type: 'error',
|
|
10
|
+
error: {
|
|
11
|
+
message: message || 'Unknown error',
|
|
12
|
+
stack: stack || '',
|
|
13
|
+
filename: filename,
|
|
14
|
+
lineno: lineno,
|
|
15
|
+
colno: colno,
|
|
16
|
+
source: source,
|
|
17
|
+
plugin: plugin
|
|
18
|
+
}
|
|
19
|
+
}, '*');
|
|
20
|
+
} catch (_) {}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Resource load errors (scripts, stylesheets)
|
|
24
|
+
window.addEventListener('error', function(e) {
|
|
25
|
+
var target = e.target || e.srcElement;
|
|
26
|
+
if (target && target !== window && target.tagName) {
|
|
27
|
+
var tag = String(target.tagName).toLowerCase();
|
|
28
|
+
var url = target.src || target.href;
|
|
29
|
+
if (url && (tag === 'script' || tag === 'link')) {
|
|
30
|
+
send(
|
|
31
|
+
(tag === 'link' ? 'Failed to load stylesheet: ' : 'Failed to load module: ') + url,
|
|
32
|
+
'', url, undefined, undefined, 'resource-load'
|
|
33
|
+
);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
send(
|
|
38
|
+
e.message || (e.error && e.error.message) || 'Runtime error occurred',
|
|
39
|
+
e.error ? e.error.stack : '',
|
|
40
|
+
e.filename, e.lineno, e.colno, 'runtime'
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Unhandled promise rejections
|
|
45
|
+
window.addEventListener('unhandledrejection', function(e) {
|
|
46
|
+
var r = e.reason;
|
|
47
|
+
send(
|
|
48
|
+
r && r.message ? r.message : String(r || 'Unhandled Promise Rejection'),
|
|
49
|
+
r && r.stack,
|
|
50
|
+
r && r.fileName,
|
|
51
|
+
r && r.lineNumber,
|
|
52
|
+
r && r.columnNumber,
|
|
53
|
+
'unhandledrejection'
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Console error interception for silent Vite/Syntax errors
|
|
58
|
+
var nativeConsoleError = console.error;
|
|
59
|
+
console.error = function() {
|
|
60
|
+
var args = Array.prototype.slice.call(arguments);
|
|
61
|
+
var msg = args.map(function(a) {
|
|
62
|
+
return typeof a === 'string' ? a : (a && a.message ? a.message : String(a));
|
|
63
|
+
}).join(' ');
|
|
64
|
+
if (/Failed to load module|Internal Server Error|Syntax Error/i.test(msg)) {
|
|
65
|
+
send(msg, '', undefined, undefined, undefined, 'console.error');
|
|
66
|
+
}
|
|
67
|
+
return nativeConsoleError.apply(console, args);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Blank screen detector
|
|
71
|
+
window.addEventListener('load', function() {
|
|
72
|
+
setTimeout(function() {
|
|
73
|
+
var root = document.getElementById('root');
|
|
74
|
+
if (root && root.children.length === 0 && !(root.textContent || '').trim()) {
|
|
75
|
+
send(
|
|
76
|
+
'Runtime error: Preview failed to render; a component or import may have failed silently.',
|
|
77
|
+
'', location.href, undefined, undefined, 'blank-screen'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}, 3000);
|
|
81
|
+
});
|
|
82
|
+
})();`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SELECTION_BRIDGE_SCRIPT = "(function(){\n if (window.__ADAPTAR_PREVIEW_SELECTION_BRIDGE__) return;\n window.__ADAPTAR_PREVIEW_SELECTION_BRIDGE__ = true;\n\n function send(type, selection) {\n try {\n window.parent.postMessage({ source: 'adaptar-preview', type: type, selection: selection || null }, '*');\n } catch (_) {}\n }\n\n function isEditableElement(el) {\n return !!el && (\n el.matches('input,textarea,select,[contenteditable=\"true\"],[data-adaptar-editable=\"true\"]') ||\n el.closest('[contenteditable=\"true\"]')\n );\n }\n\n function cssEscape(value) {\n return String(value || '').replace(/([ #;?%&,.+*~\\':\"!^$[]()=>|\\/@])/g, '\\\\$1');\n }\n\n function getText(el) {\n return String((el.innerText || el.textContent || '').replace(/\\s+/g, ' ').trim()).slice(0, 160);\n }\n\n function buildSelector(el) {\n if (!el || !el.getAttribute) return '';\n var explicit = el.getAttribute('data-adaptar-selector') || el.getAttribute('data-adaptar-ref');\n if (explicit) return explicit;\n var file = el.getAttribute('data-adaptar-file');\n if (file) return '[data-adaptar-file=\"' + cssEscape(file) + '\"]';\n var symbol = el.getAttribute('data-adaptar-symbol');\n if (symbol) return '[data-adaptar-symbol=\"' + cssEscape(symbol) + '\"]';\n if (el.id) return '#' + cssEscape(el.id);\n var parts = []; var current = el; var depth = 0;\n while (current && current !== document.body && current !== document.documentElement && depth < 5) {\n var part = current.tagName.toLowerCase();\n if (current.classList && current.classList.length) {\n part += '.' + Array.prototype.slice.call(current.classList, 0, 2).map(cssEscape).join('.');\n }\n if (current.parentElement) {\n var siblings = Array.prototype.filter.call(\n current.parentElement.children,\n function(child) { return child.tagName === current.tagName; }\n );\n if (siblings.length > 1) { part += ':nth-of-type(' + (siblings.indexOf(current) + 1) + ')'; }\n }\n parts.unshift(part); current = current.parentElement; depth += 1;\n }\n return parts.join(' > ');\n }\n\n var INSPECTABLE = 'button,a,input,textarea,select,[role=\"button\"],[role=\"link\"],[contenteditable=\"true\"],img,svg,h1,h2,h3,h4,h5,h6,p,span,div,section,article,main,nav,aside,header,footer,li,ul,ol,figure,picture,video';\n\n function findInspectableTarget(node) {\n var current = node && node.nodeType === 1 ? node : null;\n while (current) {\n if (current.hasAttribute && (\n current.hasAttribute('data-adaptar-file') ||\n current.hasAttribute('data-adaptar-symbol') ||\n current.hasAttribute('data-adaptar-selector') ||\n current.hasAttribute('data-adaptar-ref') ||\n current.hasAttribute('data-adaptar-editable') ||\n current.matches(INSPECTABLE)\n )) { return current; }\n current = current.parentElement;\n }\n return null;\n }\n\n function describeTarget(target, kind) {\n if (!target) return null;\n var rect = target.getBoundingClientRect();\n var filePath = target.getAttribute && target.getAttribute('data-adaptar-file');\n var symbolName = target.getAttribute && target.getAttribute('data-adaptar-symbol');\n var selector = buildSelector(target);\n var text = getText(target);\n var label = filePath || symbolName || (target.tagName.toLowerCase() + (text ? ' \u2014 ' + text : ''));\n return {\n source: 'preview', kind: kind, label: label,\n tagName: target.tagName.toLowerCase(),\n text: text || undefined,\n className: target.className && typeof target.className === 'string' ? target.className : undefined,\n selector: selector || undefined,\n filePath: filePath || undefined,\n symbolName: symbolName || undefined,\n bounds: { x: rect.left, y: rect.top, width: rect.width, height: rect.height },\n confidence: filePath || symbolName || selector ? 'high' : (text ? 'medium' : 'low'),\n isEditable: isEditableElement(target),\n };\n }\n\n var STYLE_ID = 'adaptar-edit-highlight';\n var HOVER_CLASS = '__adaptar_hover__';\n var editModeActive = false;\n var currentHoverTarget = null;\n var labelEl = null;\n\n var HIGHLIGHT_CSS = [\n 'body.__adaptar_edit__{cursor:crosshair!important;}',\n 'body.__adaptar_edit__ *{outline:1px solid rgba(99,179,237,0.18)!important;outline-offset:-1px;}',\n 'body.__adaptar_edit__ .__adaptar_hover__{',\n ' outline:2px solid rgba(99,179,237,0.85)!important;',\n ' outline-offset:0px;',\n ' background-color:rgba(99,179,237,0.07)!important;',\n ' position:relative;',\n '}',\n '#adaptar-hover-label{',\n ' position:fixed;z-index:2147483647;pointer-events:none;',\n ' padding:2px 7px;border-radius:4px;',\n ' background:rgba(99,179,237,0.92);color:#fff;',\n ' font:600 10px/18px -apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;',\n ' white-space:nowrap;letter-spacing:0.03em;',\n ' box-shadow:0 2px 8px rgba(0,0,0,0.22);',\n ' transform:translateY(-110%);',\n '}',\n ].join('');\n\n function injectHighlightStyles() {\n if (document.getElementById(STYLE_ID)) return;\n var s = document.createElement('style');\n s.id = STYLE_ID; s.textContent = HIGHLIGHT_CSS;\n document.head.appendChild(s);\n }\n\n function removeHighlightStyles() {\n var s = document.getElementById(STYLE_ID);\n if (s) s.parentNode.removeChild(s);\n }\n\n function createLabel() {\n if (labelEl) return;\n labelEl = document.createElement('div');\n labelEl.id = 'adaptar-hover-label';\n document.body.appendChild(labelEl);\n }\n\n function removeLabel() {\n if (labelEl) { labelEl.parentNode && labelEl.parentNode.removeChild(labelEl); labelEl = null; }\n }\n\n function updateLabel(target, rect) {\n if (!labelEl || !target) return;\n var tag = target.tagName.toLowerCase();\n var id = target.id ? ' #' + target.id : '';\n var cls = target.classList && target.classList.length ? ' .' + Array.prototype.slice.call(target.classList, 0, 2).join('.') : '';\n var file = target.getAttribute && target.getAttribute('data-adaptar-file');\n labelEl.textContent = file || ('<' + tag + id + cls + '>');\n labelEl.style.top = (rect.top + window.scrollY) + 'px';\n labelEl.style.left = (rect.left + window.scrollX) + 'px';\n }\n\n function setHoverTarget(el) {\n if (currentHoverTarget === el) return;\n if (currentHoverTarget) currentHoverTarget.classList.remove(HOVER_CLASS);\n currentHoverTarget = el;\n if (el) {\n el.classList.add(HOVER_CLASS);\n if (labelEl) { var rect = el.getBoundingClientRect(); updateLabel(el, rect); }\n } else {\n if (labelEl) labelEl.textContent = '';\n }\n }\n\n function enterEditMode() {\n if (editModeActive) return;\n editModeActive = true;\n injectHighlightStyles();\n document.body.classList.add('__adaptar_edit__');\n createLabel();\n }\n\n function exitEditMode() {\n if (!editModeActive) return;\n editModeActive = false;\n setHoverTarget(null);\n document.body.classList.remove('__adaptar_edit__');\n removeLabel();\n removeHighlightStyles();\n lastSignature = '';\n send('preview-element-clear', null);\n }\n\n var lastSignature = '';\n var lastSentAt = 0;\n var clearTimer = null;\n\n function sendHover(target) {\n var selection = describeTarget(target, 'hover');\n if (!selection) return;\n var signature = [selection.selector, selection.filePath, selection.symbolName, selection.text, selection.tagName].filter(Boolean).join('|');\n var now = Date.now();\n if (signature === lastSignature && now - lastSentAt < 80) return;\n lastSignature = signature; lastSentAt = now;\n send('preview-element-hover', selection);\n }\n\n function sendSelect(target) {\n var selection = describeTarget(target, 'selection');\n if (!selection) return;\n send('preview-element-select', selection);\n }\n\n function scheduleClear() {\n if (clearTimer) clearTimeout(clearTimer);\n clearTimer = setTimeout(function() {\n lastSignature = '';\n setHoverTarget(null);\n send('preview-element-clear', null);\n }, 40);\n }\n\n window.addEventListener('pointerover', function(event) {\n var target = findInspectableTarget(event.target);\n if (!editModeActive) { if (target) sendHover(target); return; }\n if (clearTimer) { clearTimeout(clearTimer); clearTimer = null; }\n if (!target) { setHoverTarget(null); return; }\n setHoverTarget(target);\n sendHover(target);\n }, true);\n\n window.addEventListener('click', function(event) {\n var target = findInspectableTarget(event.target);\n if (!target) return;\n if (editModeActive) event.preventDefault();\n sendSelect(target);\n }, true);\n\n window.addEventListener('mouseout', function(event) {\n if (event.relatedTarget) return;\n scheduleClear();\n }, true);\n\n window.addEventListener('blur', function() { scheduleClear(); });\n\n document.addEventListener('visibilitychange', function() {\n if (document.visibilityState !== 'visible') { scheduleClear(); }\n });\n\n window.addEventListener('message', function(event) {\n var d = event.data;\n if (!d || d.source !== 'adaptar-host') return;\n if (d.type === 'adaptar-edit-mode-enter') { enterEditMode(); }\n else if (d.type === 'adaptar-edit-mode-exit') { exitEditMode(); }\n });\n})();";
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
export const SELECTION_BRIDGE_SCRIPT = /* js */ `(function(){
|
|
2
|
+
if (window.__ADAPTAR_PREVIEW_SELECTION_BRIDGE__) return;
|
|
3
|
+
window.__ADAPTAR_PREVIEW_SELECTION_BRIDGE__ = true;
|
|
4
|
+
|
|
5
|
+
function send(type, selection) {
|
|
6
|
+
try {
|
|
7
|
+
window.parent.postMessage({ source: 'adaptar-preview', type: type, selection: selection || null }, '*');
|
|
8
|
+
} catch (_) {}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isEditableElement(el) {
|
|
12
|
+
return !!el && (
|
|
13
|
+
el.matches('input,textarea,select,[contenteditable="true"],[data-adaptar-editable="true"]') ||
|
|
14
|
+
el.closest('[contenteditable="true"]')
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function cssEscape(value) {
|
|
19
|
+
return String(value || '').replace(/([ #;?%&,.+*~\\':"!^$[]()=>|\\/@])/g, '\\\\$1');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getText(el) {
|
|
23
|
+
return String((el.innerText || el.textContent || '').replace(/\\s+/g, ' ').trim()).slice(0, 160);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildSelector(el) {
|
|
27
|
+
if (!el || !el.getAttribute) return '';
|
|
28
|
+
var explicit = el.getAttribute('data-adaptar-selector') || el.getAttribute('data-adaptar-ref');
|
|
29
|
+
if (explicit) return explicit;
|
|
30
|
+
var file = el.getAttribute('data-adaptar-file');
|
|
31
|
+
if (file) return '[data-adaptar-file="' + cssEscape(file) + '"]';
|
|
32
|
+
var symbol = el.getAttribute('data-adaptar-symbol');
|
|
33
|
+
if (symbol) return '[data-adaptar-symbol="' + cssEscape(symbol) + '"]';
|
|
34
|
+
if (el.id) return '#' + cssEscape(el.id);
|
|
35
|
+
var parts = []; var current = el; var depth = 0;
|
|
36
|
+
while (current && current !== document.body && current !== document.documentElement && depth < 5) {
|
|
37
|
+
var part = current.tagName.toLowerCase();
|
|
38
|
+
if (current.classList && current.classList.length) {
|
|
39
|
+
part += '.' + Array.prototype.slice.call(current.classList, 0, 2).map(cssEscape).join('.');
|
|
40
|
+
}
|
|
41
|
+
if (current.parentElement) {
|
|
42
|
+
var siblings = Array.prototype.filter.call(
|
|
43
|
+
current.parentElement.children,
|
|
44
|
+
function(child) { return child.tagName === current.tagName; }
|
|
45
|
+
);
|
|
46
|
+
if (siblings.length > 1) { part += ':nth-of-type(' + (siblings.indexOf(current) + 1) + ')'; }
|
|
47
|
+
}
|
|
48
|
+
parts.unshift(part); current = current.parentElement; depth += 1;
|
|
49
|
+
}
|
|
50
|
+
return parts.join(' > ');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
var INSPECTABLE = 'button,a,input,textarea,select,[role="button"],[role="link"],[contenteditable="true"],img,svg,h1,h2,h3,h4,h5,h6,p,span,div,section,article,main,nav,aside,header,footer,li,ul,ol,figure,picture,video';
|
|
54
|
+
|
|
55
|
+
function findInspectableTarget(node) {
|
|
56
|
+
var current = node && node.nodeType === 1 ? node : null;
|
|
57
|
+
while (current) {
|
|
58
|
+
if (current.hasAttribute && (
|
|
59
|
+
current.hasAttribute('data-adaptar-file') ||
|
|
60
|
+
current.hasAttribute('data-adaptar-symbol') ||
|
|
61
|
+
current.hasAttribute('data-adaptar-selector') ||
|
|
62
|
+
current.hasAttribute('data-adaptar-ref') ||
|
|
63
|
+
current.hasAttribute('data-adaptar-editable') ||
|
|
64
|
+
current.matches(INSPECTABLE)
|
|
65
|
+
)) { return current; }
|
|
66
|
+
current = current.parentElement;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function describeTarget(target, kind) {
|
|
72
|
+
if (!target) return null;
|
|
73
|
+
var rect = target.getBoundingClientRect();
|
|
74
|
+
var filePath = target.getAttribute && target.getAttribute('data-adaptar-file');
|
|
75
|
+
var symbolName = target.getAttribute && target.getAttribute('data-adaptar-symbol');
|
|
76
|
+
var selector = buildSelector(target);
|
|
77
|
+
var text = getText(target);
|
|
78
|
+
var label = filePath || symbolName || (target.tagName.toLowerCase() + (text ? ' — ' + text : ''));
|
|
79
|
+
return {
|
|
80
|
+
source: 'preview', kind: kind, label: label,
|
|
81
|
+
tagName: target.tagName.toLowerCase(),
|
|
82
|
+
text: text || undefined,
|
|
83
|
+
className: target.className && typeof target.className === 'string' ? target.className : undefined,
|
|
84
|
+
selector: selector || undefined,
|
|
85
|
+
filePath: filePath || undefined,
|
|
86
|
+
symbolName: symbolName || undefined,
|
|
87
|
+
bounds: { x: rect.left, y: rect.top, width: rect.width, height: rect.height },
|
|
88
|
+
confidence: filePath || symbolName || selector ? 'high' : (text ? 'medium' : 'low'),
|
|
89
|
+
isEditable: isEditableElement(target),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
var STYLE_ID = 'adaptar-edit-highlight';
|
|
94
|
+
var HOVER_CLASS = '__adaptar_hover__';
|
|
95
|
+
var editModeActive = false;
|
|
96
|
+
var currentHoverTarget = null;
|
|
97
|
+
var labelEl = null;
|
|
98
|
+
|
|
99
|
+
var HIGHLIGHT_CSS = [
|
|
100
|
+
'body.__adaptar_edit__{cursor:crosshair!important;}',
|
|
101
|
+
'body.__adaptar_edit__ *{outline:1px solid rgba(99,179,237,0.18)!important;outline-offset:-1px;}',
|
|
102
|
+
'body.__adaptar_edit__ .__adaptar_hover__{',
|
|
103
|
+
' outline:2px solid rgba(99,179,237,0.85)!important;',
|
|
104
|
+
' outline-offset:0px;',
|
|
105
|
+
' background-color:rgba(99,179,237,0.07)!important;',
|
|
106
|
+
' position:relative;',
|
|
107
|
+
'}',
|
|
108
|
+
'#adaptar-hover-label{',
|
|
109
|
+
' position:fixed;z-index:2147483647;pointer-events:none;',
|
|
110
|
+
' padding:2px 7px;border-radius:4px;',
|
|
111
|
+
' background:rgba(99,179,237,0.92);color:#fff;',
|
|
112
|
+
' font:600 10px/18px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;',
|
|
113
|
+
' white-space:nowrap;letter-spacing:0.03em;',
|
|
114
|
+
' box-shadow:0 2px 8px rgba(0,0,0,0.22);',
|
|
115
|
+
' transform:translateY(-110%);',
|
|
116
|
+
'}',
|
|
117
|
+
].join('');
|
|
118
|
+
|
|
119
|
+
function injectHighlightStyles() {
|
|
120
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
121
|
+
var s = document.createElement('style');
|
|
122
|
+
s.id = STYLE_ID; s.textContent = HIGHLIGHT_CSS;
|
|
123
|
+
document.head.appendChild(s);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function removeHighlightStyles() {
|
|
127
|
+
var s = document.getElementById(STYLE_ID);
|
|
128
|
+
if (s) s.parentNode.removeChild(s);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createLabel() {
|
|
132
|
+
if (labelEl) return;
|
|
133
|
+
labelEl = document.createElement('div');
|
|
134
|
+
labelEl.id = 'adaptar-hover-label';
|
|
135
|
+
document.body.appendChild(labelEl);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function removeLabel() {
|
|
139
|
+
if (labelEl) { labelEl.parentNode && labelEl.parentNode.removeChild(labelEl); labelEl = null; }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function updateLabel(target, rect) {
|
|
143
|
+
if (!labelEl || !target) return;
|
|
144
|
+
var tag = target.tagName.toLowerCase();
|
|
145
|
+
var id = target.id ? ' #' + target.id : '';
|
|
146
|
+
var cls = target.classList && target.classList.length ? ' .' + Array.prototype.slice.call(target.classList, 0, 2).join('.') : '';
|
|
147
|
+
var file = target.getAttribute && target.getAttribute('data-adaptar-file');
|
|
148
|
+
labelEl.textContent = file || ('<' + tag + id + cls + '>');
|
|
149
|
+
labelEl.style.top = (rect.top + window.scrollY) + 'px';
|
|
150
|
+
labelEl.style.left = (rect.left + window.scrollX) + 'px';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function setHoverTarget(el) {
|
|
154
|
+
if (currentHoverTarget === el) return;
|
|
155
|
+
if (currentHoverTarget) currentHoverTarget.classList.remove(HOVER_CLASS);
|
|
156
|
+
currentHoverTarget = el;
|
|
157
|
+
if (el) {
|
|
158
|
+
el.classList.add(HOVER_CLASS);
|
|
159
|
+
if (labelEl) { var rect = el.getBoundingClientRect(); updateLabel(el, rect); }
|
|
160
|
+
} else {
|
|
161
|
+
if (labelEl) labelEl.textContent = '';
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function enterEditMode() {
|
|
166
|
+
if (editModeActive) return;
|
|
167
|
+
editModeActive = true;
|
|
168
|
+
injectHighlightStyles();
|
|
169
|
+
document.body.classList.add('__adaptar_edit__');
|
|
170
|
+
createLabel();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function exitEditMode() {
|
|
174
|
+
if (!editModeActive) return;
|
|
175
|
+
editModeActive = false;
|
|
176
|
+
setHoverTarget(null);
|
|
177
|
+
document.body.classList.remove('__adaptar_edit__');
|
|
178
|
+
removeLabel();
|
|
179
|
+
removeHighlightStyles();
|
|
180
|
+
lastSignature = '';
|
|
181
|
+
send('preview-element-clear', null);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
var lastSignature = '';
|
|
185
|
+
var lastSentAt = 0;
|
|
186
|
+
var clearTimer = null;
|
|
187
|
+
|
|
188
|
+
function sendHover(target) {
|
|
189
|
+
var selection = describeTarget(target, 'hover');
|
|
190
|
+
if (!selection) return;
|
|
191
|
+
var signature = [selection.selector, selection.filePath, selection.symbolName, selection.text, selection.tagName].filter(Boolean).join('|');
|
|
192
|
+
var now = Date.now();
|
|
193
|
+
if (signature === lastSignature && now - lastSentAt < 80) return;
|
|
194
|
+
lastSignature = signature; lastSentAt = now;
|
|
195
|
+
send('preview-element-hover', selection);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function sendSelect(target) {
|
|
199
|
+
var selection = describeTarget(target, 'selection');
|
|
200
|
+
if (!selection) return;
|
|
201
|
+
send('preview-element-select', selection);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function scheduleClear() {
|
|
205
|
+
if (clearTimer) clearTimeout(clearTimer);
|
|
206
|
+
clearTimer = setTimeout(function() {
|
|
207
|
+
lastSignature = '';
|
|
208
|
+
setHoverTarget(null);
|
|
209
|
+
send('preview-element-clear', null);
|
|
210
|
+
}, 40);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
window.addEventListener('pointerover', function(event) {
|
|
214
|
+
var target = findInspectableTarget(event.target);
|
|
215
|
+
if (!editModeActive) { if (target) sendHover(target); return; }
|
|
216
|
+
if (clearTimer) { clearTimeout(clearTimer); clearTimer = null; }
|
|
217
|
+
if (!target) { setHoverTarget(null); return; }
|
|
218
|
+
setHoverTarget(target);
|
|
219
|
+
sendHover(target);
|
|
220
|
+
}, true);
|
|
221
|
+
|
|
222
|
+
window.addEventListener('click', function(event) {
|
|
223
|
+
var target = findInspectableTarget(event.target);
|
|
224
|
+
if (!target) return;
|
|
225
|
+
if (editModeActive) event.preventDefault();
|
|
226
|
+
sendSelect(target);
|
|
227
|
+
}, true);
|
|
228
|
+
|
|
229
|
+
window.addEventListener('mouseout', function(event) {
|
|
230
|
+
if (event.relatedTarget) return;
|
|
231
|
+
scheduleClear();
|
|
232
|
+
}, true);
|
|
233
|
+
|
|
234
|
+
window.addEventListener('blur', function() { scheduleClear(); });
|
|
235
|
+
|
|
236
|
+
document.addEventListener('visibilitychange', function() {
|
|
237
|
+
if (document.visibilityState !== 'visible') { scheduleClear(); }
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
window.addEventListener('message', function(event) {
|
|
241
|
+
var d = event.data;
|
|
242
|
+
if (!d || d.source !== 'adaptar-host') return;
|
|
243
|
+
if (d.type === 'adaptar-edit-mode-enter') { enterEditMode(); }
|
|
244
|
+
else if (d.type === 'adaptar-edit-mode-exit') { exitEditMode(); }
|
|
245
|
+
});
|
|
246
|
+
})();`;
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "adaptar-vite-plugin",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Vite plugin for Adaptar preview error and selection bridging",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"dev": "tsc -w"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@types/node": "^20.0.0",
|
|
14
|
+
"typescript": "^5.0.0",
|
|
15
|
+
"vite": "^7.0.0"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"vite": ">=5.0.0"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [],
|
|
21
|
+
"author": "",
|
|
22
|
+
"license": "ISC"
|
|
23
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ViteDevServer, HotPayload } from "vite";
|
|
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
|
+
}
|
|
12
|
+
|
|
13
|
+
// How long (ms) to suppress a full-reload after a fatal error so the
|
|
14
|
+
// error overlay stays visible before the iframe clears it.
|
|
15
|
+
const RELOAD_SUPPRESSION_MS = 5_000;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Intercepts Vite's internal WebSocket `send` to catch build/HMR errors
|
|
19
|
+
* at the source and forward them as structured `adaptar:error` custom events.
|
|
20
|
+
*
|
|
21
|
+
* Also suppresses the `full-reload` message that Vite fires immediately after
|
|
22
|
+
* a fatal error — without this the iframe reloads and clears the error overlay
|
|
23
|
+
* before the user has a chance to act on it.
|
|
24
|
+
*/
|
|
25
|
+
export function interceptHmrErrors(server: ViteDevServer): void {
|
|
26
|
+
const originalSend = server.ws.send.bind(server.ws);
|
|
27
|
+
|
|
28
|
+
// Tracks whether a fatal error was recently sent so we can suppress the
|
|
29
|
+
// follow-up full-reload for a short window.
|
|
30
|
+
let suppressReloadUntil = 0;
|
|
31
|
+
|
|
32
|
+
// Cast to any to override — Vite 7 exposes an overloaded signature that
|
|
33
|
+
// TypeScript cannot directly assign to. We restore full type safety inside
|
|
34
|
+
// the function body by narrowing against HotPayload ourselves.
|
|
35
|
+
(server.ws as any).send = function (payload: HotPayload): void {
|
|
36
|
+
if (payload && typeof payload === "object") {
|
|
37
|
+
// ── Fatal build / HMR error ──────────────────────────────────────────
|
|
38
|
+
if (payload.type === "error") {
|
|
39
|
+
const { err } = payload;
|
|
40
|
+
|
|
41
|
+
const formatted: AdaptarErrorPayload = {
|
|
42
|
+
message: `[vite] ${err.message || "Internal server error"}`,
|
|
43
|
+
stack: [err.stack, err.frame].filter(Boolean).join("\n\n"),
|
|
44
|
+
filename: err.id ?? err.loc?.file,
|
|
45
|
+
lineno: err.loc?.line,
|
|
46
|
+
colno: err.loc?.column,
|
|
47
|
+
source: "vite-hmr",
|
|
48
|
+
plugin: err.plugin,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
originalSend({
|
|
52
|
+
type: "custom",
|
|
53
|
+
event: "adaptar:error",
|
|
54
|
+
data: formatted,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Start suppression window so the follow-up full-reload is blocked.
|
|
58
|
+
suppressReloadUntil = Date.now() + RELOAD_SUPPRESSION_MS;
|
|
59
|
+
originalSend(payload);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Suppress full-reload that follows a fatal error ──────────────────
|
|
64
|
+
if (payload.type === "full-reload" && Date.now() < suppressReloadUntil) {
|
|
65
|
+
suppressReloadUntil = 0; // reset — only suppress once
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
originalSend(payload);
|
|
71
|
+
};
|
|
72
|
+
}
|
package/src/html-tags.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { HtmlTagDescriptor } from "vite";
|
|
2
|
+
import { ERROR_BRIDGE_SCRIPT } from "./scripts/error-bridge.js";
|
|
3
|
+
import { SELECTION_BRIDGE_SCRIPT } from "./scripts/selection-bridge.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The inline module that subscribes to the `adaptar:error` custom HMR event
|
|
7
|
+
* and forwards it to the parent window via postMessage.
|
|
8
|
+
*/
|
|
9
|
+
const HMR_LISTENER_SCRIPT = /* js */ `
|
|
10
|
+
if (import.meta.hot) {
|
|
11
|
+
import.meta.hot.on('adaptar:error', (data) => {
|
|
12
|
+
try {
|
|
13
|
+
window.parent.postMessage({ source: 'adaptar-preview', type: 'error', error: data }, '*');
|
|
14
|
+
} catch (_) {}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
`.trim();
|
|
18
|
+
|
|
19
|
+
export function buildHtmlTags(): HtmlTagDescriptor[] {
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
tag: "script",
|
|
23
|
+
injectTo: "head-prepend",
|
|
24
|
+
attrs: { "data-adaptar": "error-bridge" },
|
|
25
|
+
children: ERROR_BRIDGE_SCRIPT,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
tag: "script",
|
|
29
|
+
injectTo: "head-prepend",
|
|
30
|
+
attrs: { "data-adaptar": "selection-bridge" },
|
|
31
|
+
children: SELECTION_BRIDGE_SCRIPT,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
tag: "script",
|
|
35
|
+
attrs: { type: "module" },
|
|
36
|
+
children: HMR_LISTENER_SCRIPT,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import { interceptHmrErrors } from "./hmr-interceptor.js";
|
|
3
|
+
import { buildHtmlTags } from "./html-tags.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Adaptar Vite Plugin
|
|
7
|
+
*
|
|
8
|
+
* Provides three layers of error and selection bridging between the
|
|
9
|
+
* sandboxed preview iframe and the Adaptar host editor:
|
|
10
|
+
*
|
|
11
|
+
* 1. `error-bridge` — catches runtime JS errors, resource failures,
|
|
12
|
+
* unhandled rejections, and blank-screen scenarios.
|
|
13
|
+
* 2. `selection-bridge` — powers element inspect / edit mode in the preview.
|
|
14
|
+
* 3. `hmr-interceptor` — intercepts Vite's internal WebSocket to surface
|
|
15
|
+
* build/HMR errors as structured `adaptar:error` events.
|
|
16
|
+
*/
|
|
17
|
+
export function adaptar(): Plugin {
|
|
18
|
+
return {
|
|
19
|
+
name: "adaptar-vite-bridge",
|
|
20
|
+
enforce: "pre",
|
|
21
|
+
|
|
22
|
+
config() {
|
|
23
|
+
return {
|
|
24
|
+
server: {
|
|
25
|
+
hmr: {
|
|
26
|
+
// Disable Vite's default error overlay — Adaptar renders its own.
|
|
27
|
+
overlay: false,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
transformIndexHtml() {
|
|
34
|
+
return buildHtmlTags();
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
configureServer(server) {
|
|
38
|
+
interceptHmrErrors(server);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export const ERROR_BRIDGE_SCRIPT = /* js */ `(function(){
|
|
2
|
+
if (window.__ADAPTAR_ERROR_BRIDGE__) return;
|
|
3
|
+
window.__ADAPTAR_ERROR_BRIDGE__ = true;
|
|
4
|
+
|
|
5
|
+
function send(message, stack, filename, lineno, colno, source, plugin) {
|
|
6
|
+
try {
|
|
7
|
+
window.parent.postMessage({
|
|
8
|
+
source: 'adaptar-preview',
|
|
9
|
+
type: 'error',
|
|
10
|
+
error: {
|
|
11
|
+
message: message || 'Unknown error',
|
|
12
|
+
stack: stack || '',
|
|
13
|
+
filename: filename,
|
|
14
|
+
lineno: lineno,
|
|
15
|
+
colno: colno,
|
|
16
|
+
source: source,
|
|
17
|
+
plugin: plugin
|
|
18
|
+
}
|
|
19
|
+
}, '*');
|
|
20
|
+
} catch (_) {}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Resource load errors (scripts, stylesheets)
|
|
24
|
+
window.addEventListener('error', function(e) {
|
|
25
|
+
var target = e.target || e.srcElement;
|
|
26
|
+
if (target && target !== window && target.tagName) {
|
|
27
|
+
var tag = String(target.tagName).toLowerCase();
|
|
28
|
+
var url = target.src || target.href;
|
|
29
|
+
if (url && (tag === 'script' || tag === 'link')) {
|
|
30
|
+
send(
|
|
31
|
+
(tag === 'link' ? 'Failed to load stylesheet: ' : 'Failed to load module: ') + url,
|
|
32
|
+
'', url, undefined, undefined, 'resource-load'
|
|
33
|
+
);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
send(
|
|
38
|
+
e.message || (e.error && e.error.message) || 'Runtime error occurred',
|
|
39
|
+
e.error ? e.error.stack : '',
|
|
40
|
+
e.filename, e.lineno, e.colno, 'runtime'
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Unhandled promise rejections
|
|
45
|
+
window.addEventListener('unhandledrejection', function(e) {
|
|
46
|
+
var r = e.reason;
|
|
47
|
+
send(
|
|
48
|
+
r && r.message ? r.message : String(r || 'Unhandled Promise Rejection'),
|
|
49
|
+
r && r.stack,
|
|
50
|
+
r && r.fileName,
|
|
51
|
+
r && r.lineNumber,
|
|
52
|
+
r && r.columnNumber,
|
|
53
|
+
'unhandledrejection'
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Console error interception for silent Vite/Syntax errors
|
|
58
|
+
var nativeConsoleError = console.error;
|
|
59
|
+
console.error = function() {
|
|
60
|
+
var args = Array.prototype.slice.call(arguments);
|
|
61
|
+
var msg = args.map(function(a) {
|
|
62
|
+
return typeof a === 'string' ? a : (a && a.message ? a.message : String(a));
|
|
63
|
+
}).join(' ');
|
|
64
|
+
if (/Failed to load module|Internal Server Error|Syntax Error/i.test(msg)) {
|
|
65
|
+
send(msg, '', undefined, undefined, undefined, 'console.error');
|
|
66
|
+
}
|
|
67
|
+
return nativeConsoleError.apply(console, args);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Blank screen detector
|
|
71
|
+
window.addEventListener('load', function() {
|
|
72
|
+
setTimeout(function() {
|
|
73
|
+
var root = document.getElementById('root');
|
|
74
|
+
if (root && root.children.length === 0 && !(root.textContent || '').trim()) {
|
|
75
|
+
send(
|
|
76
|
+
'Runtime error: Preview failed to render; a component or import may have failed silently.',
|
|
77
|
+
'', location.href, undefined, undefined, 'blank-screen'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}, 3000);
|
|
81
|
+
});
|
|
82
|
+
})();`;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
export const SELECTION_BRIDGE_SCRIPT = /* js */ `(function(){
|
|
2
|
+
if (window.__ADAPTAR_PREVIEW_SELECTION_BRIDGE__) return;
|
|
3
|
+
window.__ADAPTAR_PREVIEW_SELECTION_BRIDGE__ = true;
|
|
4
|
+
|
|
5
|
+
function send(type, selection) {
|
|
6
|
+
try {
|
|
7
|
+
window.parent.postMessage({ source: 'adaptar-preview', type: type, selection: selection || null }, '*');
|
|
8
|
+
} catch (_) {}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isEditableElement(el) {
|
|
12
|
+
return !!el && (
|
|
13
|
+
el.matches('input,textarea,select,[contenteditable="true"],[data-adaptar-editable="true"]') ||
|
|
14
|
+
el.closest('[contenteditable="true"]')
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function cssEscape(value) {
|
|
19
|
+
return String(value || '').replace(/([ #;?%&,.+*~\\':"!^$[]()=>|\\/@])/g, '\\\\$1');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getText(el) {
|
|
23
|
+
return String((el.innerText || el.textContent || '').replace(/\\s+/g, ' ').trim()).slice(0, 160);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildSelector(el) {
|
|
27
|
+
if (!el || !el.getAttribute) return '';
|
|
28
|
+
var explicit = el.getAttribute('data-adaptar-selector') || el.getAttribute('data-adaptar-ref');
|
|
29
|
+
if (explicit) return explicit;
|
|
30
|
+
var file = el.getAttribute('data-adaptar-file');
|
|
31
|
+
if (file) return '[data-adaptar-file="' + cssEscape(file) + '"]';
|
|
32
|
+
var symbol = el.getAttribute('data-adaptar-symbol');
|
|
33
|
+
if (symbol) return '[data-adaptar-symbol="' + cssEscape(symbol) + '"]';
|
|
34
|
+
if (el.id) return '#' + cssEscape(el.id);
|
|
35
|
+
var parts = []; var current = el; var depth = 0;
|
|
36
|
+
while (current && current !== document.body && current !== document.documentElement && depth < 5) {
|
|
37
|
+
var part = current.tagName.toLowerCase();
|
|
38
|
+
if (current.classList && current.classList.length) {
|
|
39
|
+
part += '.' + Array.prototype.slice.call(current.classList, 0, 2).map(cssEscape).join('.');
|
|
40
|
+
}
|
|
41
|
+
if (current.parentElement) {
|
|
42
|
+
var siblings = Array.prototype.filter.call(
|
|
43
|
+
current.parentElement.children,
|
|
44
|
+
function(child) { return child.tagName === current.tagName; }
|
|
45
|
+
);
|
|
46
|
+
if (siblings.length > 1) { part += ':nth-of-type(' + (siblings.indexOf(current) + 1) + ')'; }
|
|
47
|
+
}
|
|
48
|
+
parts.unshift(part); current = current.parentElement; depth += 1;
|
|
49
|
+
}
|
|
50
|
+
return parts.join(' > ');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
var INSPECTABLE = 'button,a,input,textarea,select,[role="button"],[role="link"],[contenteditable="true"],img,svg,h1,h2,h3,h4,h5,h6,p,span,div,section,article,main,nav,aside,header,footer,li,ul,ol,figure,picture,video';
|
|
54
|
+
|
|
55
|
+
function findInspectableTarget(node) {
|
|
56
|
+
var current = node && node.nodeType === 1 ? node : null;
|
|
57
|
+
while (current) {
|
|
58
|
+
if (current.hasAttribute && (
|
|
59
|
+
current.hasAttribute('data-adaptar-file') ||
|
|
60
|
+
current.hasAttribute('data-adaptar-symbol') ||
|
|
61
|
+
current.hasAttribute('data-adaptar-selector') ||
|
|
62
|
+
current.hasAttribute('data-adaptar-ref') ||
|
|
63
|
+
current.hasAttribute('data-adaptar-editable') ||
|
|
64
|
+
current.matches(INSPECTABLE)
|
|
65
|
+
)) { return current; }
|
|
66
|
+
current = current.parentElement;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function describeTarget(target, kind) {
|
|
72
|
+
if (!target) return null;
|
|
73
|
+
var rect = target.getBoundingClientRect();
|
|
74
|
+
var filePath = target.getAttribute && target.getAttribute('data-adaptar-file');
|
|
75
|
+
var symbolName = target.getAttribute && target.getAttribute('data-adaptar-symbol');
|
|
76
|
+
var selector = buildSelector(target);
|
|
77
|
+
var text = getText(target);
|
|
78
|
+
var label = filePath || symbolName || (target.tagName.toLowerCase() + (text ? ' — ' + text : ''));
|
|
79
|
+
return {
|
|
80
|
+
source: 'preview', kind: kind, label: label,
|
|
81
|
+
tagName: target.tagName.toLowerCase(),
|
|
82
|
+
text: text || undefined,
|
|
83
|
+
className: target.className && typeof target.className === 'string' ? target.className : undefined,
|
|
84
|
+
selector: selector || undefined,
|
|
85
|
+
filePath: filePath || undefined,
|
|
86
|
+
symbolName: symbolName || undefined,
|
|
87
|
+
bounds: { x: rect.left, y: rect.top, width: rect.width, height: rect.height },
|
|
88
|
+
confidence: filePath || symbolName || selector ? 'high' : (text ? 'medium' : 'low'),
|
|
89
|
+
isEditable: isEditableElement(target),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
var STYLE_ID = 'adaptar-edit-highlight';
|
|
94
|
+
var HOVER_CLASS = '__adaptar_hover__';
|
|
95
|
+
var editModeActive = false;
|
|
96
|
+
var currentHoverTarget = null;
|
|
97
|
+
var labelEl = null;
|
|
98
|
+
|
|
99
|
+
var HIGHLIGHT_CSS = [
|
|
100
|
+
'body.__adaptar_edit__{cursor:crosshair!important;}',
|
|
101
|
+
'body.__adaptar_edit__ *{outline:1px solid rgba(99,179,237,0.18)!important;outline-offset:-1px;}',
|
|
102
|
+
'body.__adaptar_edit__ .__adaptar_hover__{',
|
|
103
|
+
' outline:2px solid rgba(99,179,237,0.85)!important;',
|
|
104
|
+
' outline-offset:0px;',
|
|
105
|
+
' background-color:rgba(99,179,237,0.07)!important;',
|
|
106
|
+
' position:relative;',
|
|
107
|
+
'}',
|
|
108
|
+
'#adaptar-hover-label{',
|
|
109
|
+
' position:fixed;z-index:2147483647;pointer-events:none;',
|
|
110
|
+
' padding:2px 7px;border-radius:4px;',
|
|
111
|
+
' background:rgba(99,179,237,0.92);color:#fff;',
|
|
112
|
+
' font:600 10px/18px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;',
|
|
113
|
+
' white-space:nowrap;letter-spacing:0.03em;',
|
|
114
|
+
' box-shadow:0 2px 8px rgba(0,0,0,0.22);',
|
|
115
|
+
' transform:translateY(-110%);',
|
|
116
|
+
'}',
|
|
117
|
+
].join('');
|
|
118
|
+
|
|
119
|
+
function injectHighlightStyles() {
|
|
120
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
121
|
+
var s = document.createElement('style');
|
|
122
|
+
s.id = STYLE_ID; s.textContent = HIGHLIGHT_CSS;
|
|
123
|
+
document.head.appendChild(s);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function removeHighlightStyles() {
|
|
127
|
+
var s = document.getElementById(STYLE_ID);
|
|
128
|
+
if (s) s.parentNode.removeChild(s);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createLabel() {
|
|
132
|
+
if (labelEl) return;
|
|
133
|
+
labelEl = document.createElement('div');
|
|
134
|
+
labelEl.id = 'adaptar-hover-label';
|
|
135
|
+
document.body.appendChild(labelEl);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function removeLabel() {
|
|
139
|
+
if (labelEl) { labelEl.parentNode && labelEl.parentNode.removeChild(labelEl); labelEl = null; }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function updateLabel(target, rect) {
|
|
143
|
+
if (!labelEl || !target) return;
|
|
144
|
+
var tag = target.tagName.toLowerCase();
|
|
145
|
+
var id = target.id ? ' #' + target.id : '';
|
|
146
|
+
var cls = target.classList && target.classList.length ? ' .' + Array.prototype.slice.call(target.classList, 0, 2).join('.') : '';
|
|
147
|
+
var file = target.getAttribute && target.getAttribute('data-adaptar-file');
|
|
148
|
+
labelEl.textContent = file || ('<' + tag + id + cls + '>');
|
|
149
|
+
labelEl.style.top = (rect.top + window.scrollY) + 'px';
|
|
150
|
+
labelEl.style.left = (rect.left + window.scrollX) + 'px';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function setHoverTarget(el) {
|
|
154
|
+
if (currentHoverTarget === el) return;
|
|
155
|
+
if (currentHoverTarget) currentHoverTarget.classList.remove(HOVER_CLASS);
|
|
156
|
+
currentHoverTarget = el;
|
|
157
|
+
if (el) {
|
|
158
|
+
el.classList.add(HOVER_CLASS);
|
|
159
|
+
if (labelEl) { var rect = el.getBoundingClientRect(); updateLabel(el, rect); }
|
|
160
|
+
} else {
|
|
161
|
+
if (labelEl) labelEl.textContent = '';
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function enterEditMode() {
|
|
166
|
+
if (editModeActive) return;
|
|
167
|
+
editModeActive = true;
|
|
168
|
+
injectHighlightStyles();
|
|
169
|
+
document.body.classList.add('__adaptar_edit__');
|
|
170
|
+
createLabel();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function exitEditMode() {
|
|
174
|
+
if (!editModeActive) return;
|
|
175
|
+
editModeActive = false;
|
|
176
|
+
setHoverTarget(null);
|
|
177
|
+
document.body.classList.remove('__adaptar_edit__');
|
|
178
|
+
removeLabel();
|
|
179
|
+
removeHighlightStyles();
|
|
180
|
+
lastSignature = '';
|
|
181
|
+
send('preview-element-clear', null);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
var lastSignature = '';
|
|
185
|
+
var lastSentAt = 0;
|
|
186
|
+
var clearTimer = null;
|
|
187
|
+
|
|
188
|
+
function sendHover(target) {
|
|
189
|
+
var selection = describeTarget(target, 'hover');
|
|
190
|
+
if (!selection) return;
|
|
191
|
+
var signature = [selection.selector, selection.filePath, selection.symbolName, selection.text, selection.tagName].filter(Boolean).join('|');
|
|
192
|
+
var now = Date.now();
|
|
193
|
+
if (signature === lastSignature && now - lastSentAt < 80) return;
|
|
194
|
+
lastSignature = signature; lastSentAt = now;
|
|
195
|
+
send('preview-element-hover', selection);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function sendSelect(target) {
|
|
199
|
+
var selection = describeTarget(target, 'selection');
|
|
200
|
+
if (!selection) return;
|
|
201
|
+
send('preview-element-select', selection);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function scheduleClear() {
|
|
205
|
+
if (clearTimer) clearTimeout(clearTimer);
|
|
206
|
+
clearTimer = setTimeout(function() {
|
|
207
|
+
lastSignature = '';
|
|
208
|
+
setHoverTarget(null);
|
|
209
|
+
send('preview-element-clear', null);
|
|
210
|
+
}, 40);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
window.addEventListener('pointerover', function(event) {
|
|
214
|
+
var target = findInspectableTarget(event.target);
|
|
215
|
+
if (!editModeActive) { if (target) sendHover(target); return; }
|
|
216
|
+
if (clearTimer) { clearTimeout(clearTimer); clearTimer = null; }
|
|
217
|
+
if (!target) { setHoverTarget(null); return; }
|
|
218
|
+
setHoverTarget(target);
|
|
219
|
+
sendHover(target);
|
|
220
|
+
}, true);
|
|
221
|
+
|
|
222
|
+
window.addEventListener('click', function(event) {
|
|
223
|
+
var target = findInspectableTarget(event.target);
|
|
224
|
+
if (!target) return;
|
|
225
|
+
if (editModeActive) event.preventDefault();
|
|
226
|
+
sendSelect(target);
|
|
227
|
+
}, true);
|
|
228
|
+
|
|
229
|
+
window.addEventListener('mouseout', function(event) {
|
|
230
|
+
if (event.relatedTarget) return;
|
|
231
|
+
scheduleClear();
|
|
232
|
+
}, true);
|
|
233
|
+
|
|
234
|
+
window.addEventListener('blur', function() { scheduleClear(); });
|
|
235
|
+
|
|
236
|
+
document.addEventListener('visibilitychange', function() {
|
|
237
|
+
if (document.visibilityState !== 'visible') { scheduleClear(); }
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
window.addEventListener('message', function(event) {
|
|
241
|
+
var d = event.data;
|
|
242
|
+
if (!d || d.source !== 'adaptar-host') return;
|
|
243
|
+
if (d.type === 'adaptar-edit-mode-enter') { enterEditMode(); }
|
|
244
|
+
else if (d.type === 'adaptar-edit-mode-exit') { exitEditMode(); }
|
|
245
|
+
});
|
|
246
|
+
})();`;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|