335 lines
9.2 KiB
JavaScript
335 lines
9.2 KiB
JavaScript
|
|
const iframeStorageBridge = (nonce) => (
|
||
|
|
/* js */
|
||
|
|
`
|
||
|
|
(function() {
|
||
|
|
const memoryStore = {};
|
||
|
|
|
||
|
|
const NONCE = ${JSON.stringify(nonce)}
|
||
|
|
|
||
|
|
const mockStorage = {
|
||
|
|
getItem: function(key) {
|
||
|
|
return memoryStore[key] !== undefined ? memoryStore[key] : null;
|
||
|
|
},
|
||
|
|
setItem: function(key, value) {
|
||
|
|
memoryStore[key] = String(value);
|
||
|
|
window.parent.postMessage({
|
||
|
|
type: 'storage-set',
|
||
|
|
key: key,
|
||
|
|
value: String(value),
|
||
|
|
nonce: NONCE
|
||
|
|
}, '*');
|
||
|
|
},
|
||
|
|
removeItem: function(key) {
|
||
|
|
delete memoryStore[key];
|
||
|
|
window.parent.postMessage({
|
||
|
|
type: 'storage-remove',
|
||
|
|
key: key,
|
||
|
|
nonce: NONCE
|
||
|
|
}, '*');
|
||
|
|
},
|
||
|
|
clear: function() {
|
||
|
|
for (const key in memoryStore) {
|
||
|
|
delete memoryStore[key];
|
||
|
|
}
|
||
|
|
window.parent.postMessage({
|
||
|
|
type: 'storage-clear',
|
||
|
|
nonce: NONCE
|
||
|
|
}, '*');
|
||
|
|
},
|
||
|
|
key: function(index) {
|
||
|
|
const keys = Object.keys(memoryStore);
|
||
|
|
return keys[index] !== undefined ? keys[index] : null;
|
||
|
|
},
|
||
|
|
get length() {
|
||
|
|
return Object.keys(memoryStore).length;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
try {
|
||
|
|
Object.defineProperty(window, 'localStorage', {
|
||
|
|
value: mockStorage,
|
||
|
|
writable: false,
|
||
|
|
configurable: true
|
||
|
|
});
|
||
|
|
} catch (e) {
|
||
|
|
window.localStorage = mockStorage;
|
||
|
|
}
|
||
|
|
|
||
|
|
window.addEventListener('message', function(event) {
|
||
|
|
if (event.data.type === 'storage-sync-data' && event.data.nonce === NONCE) {
|
||
|
|
const data = event.data.data;
|
||
|
|
for (const key in data) {
|
||
|
|
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
||
|
|
memoryStore[key] = data[key];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (typeof window.initTheme === 'function') {
|
||
|
|
window.initTheme();
|
||
|
|
}
|
||
|
|
window.dispatchEvent(new Event('storage-ready'));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
window.parent.postMessage({
|
||
|
|
type: 'storage-sync-request',
|
||
|
|
nonce: NONCE
|
||
|
|
}, '*');
|
||
|
|
})();
|
||
|
|
`
|
||
|
|
);
|
||
|
|
const parentStorageBridge = (nonce) => (
|
||
|
|
/* js */
|
||
|
|
`
|
||
|
|
(function() {
|
||
|
|
const host = document.querySelector('nuxt-error-overlay');
|
||
|
|
if (!host) return;
|
||
|
|
|
||
|
|
// Wait for shadow root to be attached
|
||
|
|
const checkShadow = setInterval(function() {
|
||
|
|
if (host.shadowRoot) {
|
||
|
|
clearInterval(checkShadow);
|
||
|
|
const iframe = host.shadowRoot.getElementById('frame');
|
||
|
|
if (!iframe) return;
|
||
|
|
|
||
|
|
const NONCE = ${JSON.stringify(nonce)}
|
||
|
|
|
||
|
|
window.addEventListener('message', function(event) {
|
||
|
|
if (!event.data || event.data.nonce !== NONCE) return;
|
||
|
|
|
||
|
|
const data = event.data;
|
||
|
|
|
||
|
|
if (data.type === 'storage-set') {
|
||
|
|
localStorage.setItem(data.key, data.value);
|
||
|
|
} else if (data.type === 'storage-remove') {
|
||
|
|
localStorage.removeItem(data.key);
|
||
|
|
} else if (data.type === 'storage-clear') {
|
||
|
|
localStorage.clear();
|
||
|
|
} else if (data.type === 'storage-sync-request') {
|
||
|
|
const allData = {};
|
||
|
|
for (let i = 0; i < localStorage.length; i++) {
|
||
|
|
const key = localStorage.key(i);
|
||
|
|
allData[key] = localStorage.getItem(key);
|
||
|
|
}
|
||
|
|
iframe.contentWindow.postMessage({
|
||
|
|
type: 'storage-sync-data',
|
||
|
|
data: allData,
|
||
|
|
nonce: NONCE
|
||
|
|
}, '*');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}, 10);
|
||
|
|
})();
|
||
|
|
`
|
||
|
|
);
|
||
|
|
const errorCSS = (
|
||
|
|
/* css */
|
||
|
|
`
|
||
|
|
:host {
|
||
|
|
--preview-width: 240px;
|
||
|
|
--preview-height: 180px;
|
||
|
|
--base-width: 1200px;
|
||
|
|
--base-height: 900px;
|
||
|
|
--z-base: 999999998;
|
||
|
|
all: initial;
|
||
|
|
display: contents;
|
||
|
|
}
|
||
|
|
.sr-only {
|
||
|
|
position: absolute;
|
||
|
|
width: 1px;
|
||
|
|
height: 1px;
|
||
|
|
padding: 0;
|
||
|
|
margin: -1px;
|
||
|
|
overflow: hidden;
|
||
|
|
clip: rect(0, 0, 0, 0);
|
||
|
|
white-space: nowrap;
|
||
|
|
border-width: 0;
|
||
|
|
}
|
||
|
|
#frame {
|
||
|
|
position: fixed;
|
||
|
|
left: 0;
|
||
|
|
top: 0;
|
||
|
|
width: 100vw;
|
||
|
|
height: 100vh;
|
||
|
|
border: none;
|
||
|
|
z-index: var(--z-base);
|
||
|
|
}
|
||
|
|
#frame[inert] {
|
||
|
|
right: 5px;
|
||
|
|
bottom: 5px;
|
||
|
|
left: auto;
|
||
|
|
top: auto;
|
||
|
|
width: var(--base-width);
|
||
|
|
height: var(--base-height);
|
||
|
|
transform: scale(calc(240 / 1200));
|
||
|
|
transform-origin: bottom right;
|
||
|
|
overflow: hidden;
|
||
|
|
border-radius: calc(1200 * 8px / 240);
|
||
|
|
}
|
||
|
|
#preview {
|
||
|
|
position: fixed;
|
||
|
|
right: 5px;
|
||
|
|
bottom: 5px;
|
||
|
|
width: var(--preview-width);
|
||
|
|
height: var(--preview-height);
|
||
|
|
overflow: hidden;
|
||
|
|
border-radius: 8px;
|
||
|
|
pointer-events: none;
|
||
|
|
z-index: var(--z-base);
|
||
|
|
background: white;
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
#frame:not([inert]) + #preview {
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
#toggle {
|
||
|
|
position: fixed;
|
||
|
|
right: 5px;
|
||
|
|
bottom: 5px;
|
||
|
|
width: var(--preview-width);
|
||
|
|
height: var(--preview-height);
|
||
|
|
background: none;
|
||
|
|
border: 3px solid #00DC82;
|
||
|
|
border-radius: 8px;
|
||
|
|
cursor: pointer;
|
||
|
|
opacity: 0.8;
|
||
|
|
transition: opacity 0.2s, box-shadow 0.2s;
|
||
|
|
z-index: calc(var(--z-base) + 1);
|
||
|
|
}
|
||
|
|
#toggle:hover,
|
||
|
|
#toggle:focus {
|
||
|
|
opacity: 1;
|
||
|
|
box-shadow: 0 0 20px rgba(0, 220, 130, 0.6);
|
||
|
|
}
|
||
|
|
#toggle:focus-visible {
|
||
|
|
outline: 3px solid #00DC82;
|
||
|
|
outline-offset: 3px;
|
||
|
|
box-shadow: 0 0 24px rgba(0, 220, 130, 0.8);
|
||
|
|
}
|
||
|
|
@media (prefers-reduced-motion: reduce) {
|
||
|
|
#toggle {
|
||
|
|
transition: none;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
`
|
||
|
|
);
|
||
|
|
function webComponentScript(base64HTML, startMinimized) {
|
||
|
|
return (
|
||
|
|
/* js */
|
||
|
|
`
|
||
|
|
(function() {
|
||
|
|
try {
|
||
|
|
const host = document.querySelector('nuxt-error-overlay');
|
||
|
|
if (!host) return;
|
||
|
|
|
||
|
|
const shadow = host.attachShadow({ mode: 'open' });
|
||
|
|
|
||
|
|
// Create elements
|
||
|
|
const style = document.createElement('style');
|
||
|
|
style.textContent = ${JSON.stringify(errorCSS)};
|
||
|
|
|
||
|
|
const iframe = document.createElement('iframe');
|
||
|
|
iframe.id = 'frame';
|
||
|
|
iframe.src = 'data:text/html;base64,${base64HTML}';
|
||
|
|
iframe.title = 'Detailed error stack trace';
|
||
|
|
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');
|
||
|
|
|
||
|
|
const preview = document.createElement('div');
|
||
|
|
preview.id = 'preview';
|
||
|
|
|
||
|
|
const button = document.createElement('button');
|
||
|
|
button.id = 'toggle';
|
||
|
|
button.setAttribute('aria-expanded', 'true');
|
||
|
|
button.setAttribute('type', 'button');
|
||
|
|
button.innerHTML = '<span class="sr-only">Toggle detailed error view</span>';
|
||
|
|
|
||
|
|
const liveRegion = document.createElement('div');
|
||
|
|
liveRegion.setAttribute('role', 'status');
|
||
|
|
liveRegion.setAttribute('aria-live', 'polite');
|
||
|
|
liveRegion.className = 'sr-only';
|
||
|
|
|
||
|
|
// Update preview snapshot
|
||
|
|
function updatePreview() {
|
||
|
|
try {
|
||
|
|
let previewIframe = preview.querySelector('iframe');
|
||
|
|
if (!previewIframe) {
|
||
|
|
previewIframe = document.createElement('iframe');
|
||
|
|
previewIframe.style.cssText = 'width: 1200px; height: 900px; transform: scale(0.2); transform-origin: top left; border: none;';
|
||
|
|
previewIframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');
|
||
|
|
preview.appendChild(previewIframe);
|
||
|
|
}
|
||
|
|
|
||
|
|
const doctype = document.doctype ? '<!DOCTYPE ' + document.doctype.name + '>' : '';
|
||
|
|
const cleanedHTML = document.documentElement.outerHTML
|
||
|
|
.replace(/<nuxt-error-overlay[^>]*>.*?<\\/nuxt-error-overlay>/gs, '')
|
||
|
|
.replace(/<script[^>]*>.*?<\\/script>/gs, '');
|
||
|
|
|
||
|
|
const iframeDoc = previewIframe.contentDocument || previewIframe.contentWindow.document;
|
||
|
|
iframeDoc.open();
|
||
|
|
iframeDoc.write(doctype + cleanedHTML);
|
||
|
|
iframeDoc.close();
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Failed to update preview:', error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggleView() {
|
||
|
|
const isMinimized = iframe.hasAttribute('inert');
|
||
|
|
|
||
|
|
if (isMinimized) {
|
||
|
|
updatePreview();
|
||
|
|
iframe.removeAttribute('inert');
|
||
|
|
button.setAttribute('aria-expanded', 'true');
|
||
|
|
liveRegion.textContent = 'Showing detailed error view';
|
||
|
|
setTimeout(function() {
|
||
|
|
try { iframe.contentWindow.focus(); } catch {}
|
||
|
|
}, 100);
|
||
|
|
} else {
|
||
|
|
iframe.setAttribute('inert', '');
|
||
|
|
button.setAttribute('aria-expanded', 'false');
|
||
|
|
liveRegion.textContent = 'Showing error page';
|
||
|
|
button.focus();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
button.onclick = toggleView;
|
||
|
|
|
||
|
|
document.addEventListener('keydown', function(e) {
|
||
|
|
if ((e.key === 'Escape' || e.key === 'Esc') && !iframe.hasAttribute('inert')) {
|
||
|
|
toggleView();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Append to shadow DOM
|
||
|
|
shadow.appendChild(style);
|
||
|
|
shadow.appendChild(liveRegion);
|
||
|
|
shadow.appendChild(iframe);
|
||
|
|
shadow.appendChild(preview);
|
||
|
|
shadow.appendChild(button);
|
||
|
|
|
||
|
|
if (${startMinimized}) {
|
||
|
|
iframe.setAttribute('inert', '');
|
||
|
|
button.setAttribute('aria-expanded', 'false');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Initialize preview
|
||
|
|
setTimeout(updatePreview, 100);
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Failed to initialize Nuxt error overlay:', error);
|
||
|
|
}
|
||
|
|
})();
|
||
|
|
`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
export function generateErrorOverlayHTML(html, options) {
|
||
|
|
const nonce = Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, "0")).join("");
|
||
|
|
const errorPage = html.replace("<head>", `<head><script>${iframeStorageBridge(nonce)}<\/script>`);
|
||
|
|
const base64HTML = Buffer.from(errorPage, "utf8").toString("base64");
|
||
|
|
return `
|
||
|
|
<script>${parentStorageBridge(nonce)}<\/script>
|
||
|
|
<nuxt-error-overlay></nuxt-error-overlay>
|
||
|
|
<script>${webComponentScript(base64HTML, options?.startMinimized ?? false)}<\/script>
|
||
|
|
`;
|
||
|
|
}
|