utquidem
Version:
The meta-framework suite designed from scratch for frontend-focused modern web development.
302 lines (265 loc) • 7.12 kB
text/typescript
// 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}`;