import { addMessageListener } from './error-overlay/websocket' export default function initializeBuildWatcher( toggleCallback, position = 'bottom-right' ) { const shadowHost = document.createElement('div') const [verticalProperty, horizontalProperty] = position.split('-') shadowHost.id = '__next-build-watcher' // Make sure container is fixed and on a high zIndex so it shows shadowHost.style.position = 'fixed' // Ensure container's position to be top or bottom (default) shadowHost.style[verticalProperty] = '10px' // Ensure container's position to be left or right (default) shadowHost.style[horizontalProperty] = '20px' shadowHost.style.width = 0 shadowHost.style.height = 0 shadowHost.style.zIndex = 99999 document.body.appendChild(shadowHost) let shadowRoot let prefix = '' if (shadowHost.attachShadow) { shadowRoot = shadowHost.attachShadow({ mode: 'open' }) } else { // If attachShadow is undefined then the browser does not support // the Shadow DOM, we need to prefix all the names so there // will be no conflicts shadowRoot = shadowHost prefix = '__next-build-watcher-' } // Container const container = createContainer(prefix) shadowRoot.appendChild(container) // CSS const css = createCss(prefix, { horizontalProperty, verticalProperty }) shadowRoot.appendChild(css) // State let isVisible = false let isBuilding = false let timeoutId = null // Handle events addMessageListener((event) => { // This is the heartbeat event if (event.data === '\uD83D\uDC93') { return } try { handleMessage(event) } catch {} }) function handleMessage(event) { const obj = typeof event === 'string' ? { action: event } : JSON.parse(event.data) // eslint-disable-next-line default-case switch (obj.action) { case 'building': timeoutId && clearTimeout(timeoutId) isVisible = true isBuilding = true updateContainer() break case 'built': case 'sync': isBuilding = false // Wait for the fade out transition to complete timeoutId = setTimeout(() => { isVisible = false updateContainer() }, 100) updateContainer() break } } toggleCallback(handleMessage) function updateContainer() { if (isBuilding) { container.classList.add(`${prefix}building`) } else { container.classList.remove(`${prefix}building`) } if (isVisible) { container.classList.add(`${prefix}visible`) } else { container.classList.remove(`${prefix}visible`) } } } function createContainer(prefix) { const container = document.createElement('div') container.id = `${prefix}container` container.innerHTML = `
` return container } function createCss(prefix, { horizontalProperty, verticalProperty }) { const css = document.createElement('style') css.textContent = ` #${prefix}container { position: absolute; ${verticalProperty}: 10px; ${horizontalProperty}: 30px; border-radius: 3px; background: #000; color: #fff; font: initial; cursor: initial; letter-spacing: initial; text-shadow: initial; text-transform: initial; visibility: initial; padding: 7px 10px 8px 10px; align-items: center; box-shadow: 0 11px 40px 0 rgba(0, 0, 0, 0.25), 0 2px 10px 0 rgba(0, 0, 0, 0.12); display: none; opacity: 0; transition: opacity 0.1s ease, ${verticalProperty} 0.1s ease; animation: ${prefix}fade-in 0.1s ease-in-out; } #${prefix}container.${prefix}visible { display: flex; } #${prefix}container.${prefix}building { ${verticalProperty}: 20px; opacity: 1; } #${prefix}icon-wrapper { width: 16px; height: 16px; } #${prefix}icon-wrapper > svg { width: 100%; height: 100%; } #${prefix}icon-group { animation: ${prefix}strokedash 1s ease-in-out both infinite; } @keyframes ${prefix}fade-in { from { ${verticalProperty}: 10px; opacity: 0; } to { ${verticalProperty}: 20px; opacity: 1; } } @keyframes ${prefix}strokedash { 0% { stroke-dasharray: 0 226; } 80%, 100% { stroke-dasharray: 659 226; } } ` return css }