console.log("Loaded HMR"); var eventSource = null; /** @type {Promise<() => void>} */ let updateAppContentJson = new Promise((resolve, reject) => resolve(() => {})); function connect(sendContentJsonPort, initialErrorPage) { let reconnectFrequencySeconds = 1; // reconnect logic based on: https://stackoverflow.com/a/61148682/383983 // Listen for the server to tell us that an HMR update is available function waitFunc() { return reconnectFrequencySeconds * 1000; } function tryToSetupFunc() { setupEventSource(); reconnectFrequencySeconds *= 2; if (reconnectFrequencySeconds >= 8) { reconnectFrequencySeconds = 8; } } function reconnectFunc() { console.log( `Attempting dev server reconnect in ${reconnectFrequencySeconds}...` ); setTimeout(tryToSetupFunc, waitFunc()); } function setupEventSource() { eventSource = new EventSource("/stream"); window.reloadOnOk = initialErrorPage; try { if (initialErrorPage) { handleEvent(sendContentJsonPort, { data: "content.dat" }); } } catch (e) {} eventSource.onopen = async function () { hideError(); reconnectFrequencySeconds = 1; }; eventSource.onerror = async function (evt) { eventSource && eventSource.close(); reconnectFunc(); showReconnectBanner(); }; eventSource.onmessage = async function (evt) { handleEvent(sendContentJsonPort, evt); }; } setupEventSource(); } function showReconnectBanner() { showError({ type: "compile-errors", errors: [ { path: "", name: "", problems: [ { title: "", // region: "", message: ["Dev server is disconnected..."], }, ], }, ], }); } async function handleEvent(sendContentJsonPort, evt) { if (evt.data === "content.dat") { showCompiling(""); const elmJsRequest = elmJsFetch(); const fetchContentJson = fetchContentJsonForCurrentPage(); updateAppContentJson = updateContentJsonWith( fetchContentJson, sendContentJsonPort ); try { await fetchContentJson; thenApplyHmr(await elmJsRequest); } catch (errorJson) { if (typeof errorJson === "string") { errorJson = JSON.parse(errorJson); } if (errorJson.type) { showError(errorJson); } else if (errorJson.length > 0) { showError({ type: "compile-errors", errors: errorJson, }); } else { showError(JSON.parse(errorJson.errorsJson.errors)); } } } else if (evt.data === "elm.js") { showCompiling(""); elmJsFetch().then(thenApplyHmr); } else { console.log("Unhandled", evt.data); } } /** * * @param {*} fetchContentJsonPromise * @param {*} sendContentJsonPort * @returns {Promise<() => void>} */ async function updateContentJsonWith( fetchContentJsonPromise, sendContentJsonPort ) { return new Promise(async (resolve, reject) => { try { const newContentJson = await fetchContentJsonPromise; hideError(); resolve(() => { sendContentJsonPort(newContentJson); hideCompiling("fast"); }); } catch (errorJson) { if (errorJson.type) { showError(errorJson); } else if (typeof errorJson === "string") { showError(JSON.parse(errorJson)); } else { showError(errorJson); } } }); } function fetchContentJsonForCurrentPage() { return new Promise(async (resolve, reject) => { let currentPath = window.location.pathname.replace(/(\w)$/, "$1/"); const contentJsonForPage = await fetch( `${window.location.origin}${currentPath}content.dat` ); if (contentJsonForPage.ok || contentJsonForPage.status === 404) { resolve( new DataView(await (await contentJsonForPage.blob()).arrayBuffer()) ); } else { try { reject(await contentJsonForPage.json()); } catch (error) { resolve(null); } } }); } // Expose the Webpack HMR API // var myDisposeCallback = null; var myDisposeCallback = function () { console.log("dispose..."); }; // simulate the HMR api exposed by webpack var module = { hot: { accept: async function () { (await updateAppContentJson)(); }, dispose: function (callback) { myDisposeCallback = callback; }, data: null, apply: function () { var newData = {}; myDisposeCallback(newData); module.hot.data = newData; }, verbose: true, }, }; // Thanks to the elm-live maintainers and contributors for this code for rendering errors as an HTML overlay // https://github.com/wking-io/elm-live/blob/e317b4914c471addea7243c47f28dcebe27a5d36/lib/src/websocket.js const pipe = (...fns) => (x) => fns.reduce((y, f) => f(y), x); function elmJsFetch() { var elmJsRequest = new Request("/elm.js", { cache: "no-cache" }); return fetch(elmJsRequest); } async function waitFor(millis) { return new Promise((resolve) => { setTimeout(resolve, millis); }); } async function thenApplyHmr(response) { if (response.ok) { if (window.reloadOnOk) { location.reload(); } else { response.text().then(function (value) { module.hot.apply(); delete window.Elm; eval(value); }); } } else { try { const errorJson = await response.json(); console.error("JSON", errorJson); showError(errorJson); } catch (jsonParsingError) { console.log("Couldn't parse error", jsonParsingError); } } } function colorConverter(color) { return { black: "#000000", red: "#F77F00", green: "#33ff00", yellow: "#ffff00", blue: "#99B1BC", magenta: "#cc00ff", cyan: "#00ffff", white: "#d0d0d0", BLACK: "#808080", RED: "#ff0000", GREEN: "#33ff00", YELLOW: "#ffff00", BLUE: "#0066ff", MAGENTA: "#cc00ff", CYAN: "#00ffff", WHITE: "#ffffff", }[color]; } const addNewLine = (str) => str + "\n"; const styleColor = (str = "WHITE") => `color: ${colorConverter(str) || str};`; const styleUnderline = `text-decoration: underline;`; const styleBold = `text-decoration: bold;`; const parseStyle = ({ underline, color, bold }) => `${underline ? styleUnderline : ""}${color ? styleColor(color) : ""}${ bold ? styleBold : "" }`; function capitalizeFirstLetter(str) { return str.charAt(0).toUpperCase() + str.slice(1); } function consoleSanitize(str) { return str.replace(/<(http[^>]*)>/, "$1"); } function htmlSanitize(str, type) { var temp = document.createElement("div"); temp.textContent = str; return temp.innerHTML.replace( /<(http[^>]*)>/, "<$1>" ); } function parseHeader(title, path) { return `-- ${(title || "ERROR").replace("-", " ")} --------------- ${ path || "" }`; } /* |------------------------------------------------------------------------------- | Console Logging |------------------------------------------------------------------------------- */ const wrapConsole = (str) => `%c${str}`; const consoleHeader = pipe(parseHeader, wrapConsole, addNewLine, addNewLine); const parseMsg = pipe(consoleSanitize, wrapConsole); const consoleMsg = ({ error, style }, msg) => ({ error: error.concat(parseMsg(typeof msg === "string" ? msg : msg.string)), style: style.concat( parseStyle(typeof msg === "string" ? { color: "black" } : msg) ), }); const joinMessage = ({ error, style }) => [error.join("")].concat(style); const parseConsoleErrors = (path) => /** * @param {{ title: string; message: Message[]}} info * */ (info) => { if (info.rule) { if (info.details) { return joinMessage( info.details.reduce(consoleMsg, { error: [consoleHeader(info.rule, path)], style: [styleColor("blue")], }) ); } return joinMessage( info.formatted.reduce(consoleMsg, { error: [consoleHeader(info.rule, path)], style: [styleColor("blue")], }) ); } else { return joinMessage( info.message.reduce(consoleMsg, { error: [consoleHeader(info.title, path)], style: [styleColor("blue")], }) ); } }; /** * @param {RootObject} error * */ const restoreColorConsole = (error) => { if (error.type === "compile-errors" && error.errors) { return error.errors.reduce( (acc, { problems, path }) => acc.concat(problems.map(parseConsoleErrors(path))), [] ); } else if (error.type === "review-errors" && error.errors) { return error.errors.reduce( (acc, { errors, path }) => acc.concat(errors.map(parseConsoleErrors(path))), [] ); } else if (error.type === "error") { return parseConsoleErrors(error.path)(error); } else { console.error(`Unknown error type ${error}`); } }; /* |------------------------------------------------------------------------------- | Html Logging |------------------------------------------------------------------------------- */ const htmlHeader = (title, path) => `${parseHeader( title, path )}\n\n`; const htmlMsg = (acc, msg) => `${acc}${htmlSanitize(typeof msg === "string" ? msg : msg.string)}`; const parseHtmlErrors = (path) => (info) => { if (info.rule) { return info.formatted.reduce(htmlMsg, htmlHeader(info.rule, path)); } else { if (info.details) { return info.details.reduce(htmlMsg, htmlHeader(info.title, path)); } return info.message.reduce(htmlMsg, htmlHeader(info.title, path)); } }; const restoreColorHtml = /** * @param {RootObject} error * */ (error) => { if (error.type === "compile-errors") { return error.errors.reduce( (acc, { problems, path }) => acc.concat(problems.map(parseHtmlErrors(path))), [] ); } else if (error.type === "review-errors") { return error.errors.reduce( (acc, { errors, path }) => acc.concat(errors.map(parseHtmlErrors(path))), [] ); } else if (error.type === "error") { return parseHtmlErrors(error.path)(error); } else { throw new Error(`Unknown error type ${error}`); } }; /* |------------------------------------------------------------------------------- | TODO: Refactor Below |------------------------------------------------------------------------------- */ var speed = 400; var delay = 20; /** * @param {RootObject} error */ function showError(error) { try { restoreColorConsole(error).forEach((error) => { console.log.apply(this, error); }); } catch (e) { console.log("Error", error); } hideCompiling("fast"); setTimeout(function () { showError_(restoreColorHtml(error)); }, delay); } function showError_(error) { var nodeContainer = document.getElementById("elm-live:elmErrorContainer"); if (!nodeContainer) { nodeContainer = document.createElement("div"); nodeContainer.id = "elm-live:elmErrorContainer"; document.body.appendChild(nodeContainer); } nodeContainer.innerHTML = `
${error}