UNPKG

utquidem

Version:

The meta-framework suite designed from scratch for frontend-focused modern web development.

302 lines (265 loc) 7.12 kB
// css HMR const styles = new Map(); export const removeStyle = id => { if (styles.has(id)) { document.head.removeChild(styles.get(id)); styles.delete(id); } }; export const updateStyle = (id, code) => { const style = styles.get(id); if (!style) { const styleEl = document.createElement('style'); const codeEl = document.createTextNode(code); styleEl.type = 'text/css'; styleEl.appendChild(codeEl); document.head.appendChild(styleEl); styles.set(id, styleEl); } else { style.innerHTML = code; } }; const ERROR_OVERLAY_NAME = 'modern-js-error-overlay'; // check if has error overlay function hasErrorOverlay() { return document.querySelectorAll(ERROR_OVERLAY_NAME).length; } // clear react error overlay function clearErrorOverlay() { document.querySelectorAll(ERROR_OVERLAY_NAME).forEach(el => el.remove()); } // create react error overlay function createErrorOverlay(data) { const ErrorOverlay = customElements.get(ERROR_OVERLAY_NAME); if (ErrorOverlay) { const overlay = new ErrorOverlay(data); clearErrorOverlay(); document.body.appendChild(overlay); } } type LoggerFunction = (...args: any[]) => void; type LoggerType = { info: LoggerFunction; error: LoggerFunction; warn: LoggerFunction; }; const logger = [`info`, 'error', 'warn'].reduce((m, c) => { m[c] = (...params) => // eslint-disable-next-line no-console console[c === 'info' ? 'log' : c](`[HMR]:`, ...params); return m; }, {} as LoggerType); let isFisrtUpdate = true; const handleMessage = event => { const data = JSON.parse(event.data); switch (data.type) { case 'connected': { logger.info(`Connected`); // TODO: socket ping break; } case 'reload': { window.location.reload(); break; } case 'update': { // if there is already a error in first update // which meanings some resource load failed // so should reload page to ensure app run correctly if (isFisrtUpdate && hasErrorOverlay()) { window.location.reload(); return; } isFisrtUpdate = false; try { const timestamp = data.hmrTimestamp; for (const { id, accepted } of data.changes) { doAccept(id, accepted, timestamp); } clearErrorOverlay(); } catch (err) { logger.error('Hot Update Error', err); createErrorOverlay({ title: 'Hot Update Error', loc: { file: data.url || '', }, message: err.message, stack: err.stack, }); } break; } // handle removed dependencies after hmr update // eg: remove style tags in head element case 'prune': { for (const id of data.changes) { doPrune(id); } break; } case 'error': { logger.error(data.message); createErrorOverlay(data); break; } case 'close': { logger.info(`Disconnected!`); break; } default: { logger.error(`Invalid message type:`, data); } } }; const handleError = err => { logger.error(`connection error`, err); }; let retries = 0; const handleClose = () => { logger.info(`Disconnected!`); if (retries <= 10) { retries += 1; setTimeout(() => { initSocket(); }, 1000 * Math.pow(2, retries) + Math.random() * 100); } }; // init websocket client connection const initSocket = () => { const ws = new WebSocket( `${location.origin.replace(/http/, 'ws')}/_modern_js_hmr_ws`, ); ws.onerror = handleError; ws.onmessage = handleMessage; ws.onclose = handleClose; }; initSocket(); const strip = url => url.replace(/(\?.*$|#.*$)/, ''); const noop = function () { /** */ }; const modules = new Map(); let updateQueue = []; let updating = false; const doAccept = async (id, acceptedPath, timestamp) => { if (!updateQueue.includes(id)) { updateQueue.push({ id, acceptedPath }); } if (!updating) { updating = true; await Promise.resolve(); updating = false; const loading = updateQueue.map(({ id: path, acceptedPath: accepted }) => update(path, accepted, timestamp), ); updateQueue = []; await Promise.all(loading); } }; const update = async (id, acceptedPath, timestamp) => { if (!modules.has(id)) { return; } const mod = modules.get(id); let callbacks = []; const modulesToUpdate = new Set(); if (mod._selfAccepted) { modulesToUpdate.add(id); callbacks = [mod._selfAccepted]; } else { modulesToUpdate.add(id); mod._acceptedCallbacks.forEach(({ deps, callback }) => { if (deps.some(dep => dep === acceptedPath)) { modulesToUpdate.add(acceptedPath); callbacks.push(callback); } }); } const loadedModules = await Promise.all( Array.from(modulesToUpdate).map(m => { const newUrl = __addQuery__(m, `t=${timestamp}`); return import(newUrl); }), ); for (const callback of callbacks) { typeof callback === 'function' && callback(loadedModules); } logger.info(`hot updated: accepted via ${id}`); }; /** Called when a new module is loaded, to run cleanup on the old module (if needed) */ const doDispose = id => { const mod = modules.get(id); const callbacks = mod._disposeCallbacks; callbacks.map(callback => callback()); }; const doPrune = id => { if (modules.has(id)) { const mod = modules.get(id); const callbacks = mod._prunCallbacks; callbacks.map(callback => callback()); modules.delete(id); } }; export const createHotContext = url => { const id = strip(url); const mod = modules.get(id); if (mod) { // mod.lock(); doDispose(mod._id); mod._disposeCallbacks = []; mod._acceptedCallbacks = []; mod._selfAccepted = false; return mod; } const hot = { // private _id: id, _declinedDeps: {}, _acceptedDeps: {}, _selfAccepted: false, _selfDeclined: false, _disposeCallbacks: [], _acceptedCallbacks: [], _prunCallbacks: [], // hot API accept(dep, callback) { if (dep === undefined) { hot._selfAccepted = true; } else if (typeof dep === 'function') { hot._selfAccepted = dep; } else if (Array.isArray(dep)) { hot._acceptedCallbacks.push({ deps: dep, callback: callback || noop }); } else { hot._acceptedCallbacks.push({ deps: [dep], callback: callback || noop, }); } }, decline(dep) { if (dep === undefined) { hot._selfDeclined = true; } else if (Array.isArray(dep)) { for (const d of dep) { hot._acceptedDeps[d] = true; } } else { hot._declinedDeps[dep] = true; } }, dispose(callback) { hot._disposeCallbacks.push(callback); }, prune(callback) { hot._prunCallbacks.push(callback); }, invalidate() { window.location.reload(); }, }; modules.set(id, hot); return hot; }; export const __addQuery__ = (url, query) => /\?.*/.test(url) ? `${url}&${query}` : `${url}?${query}`;