UNPKG

@dillonkearns/elm-graphql

Version:

<img src="https://cdn.jsdelivr.net/gh/martimatix/logo-graphqelm/logo.svg" alt="dillonearns/elm-graphql logo" width="40%" align="right">

501 lines (440 loc) 21.6 kB
//////////////////// HMR BEGIN //////////////////// /* MIT License http://www.opensource.org/licenses/mit-license.php Original Author: Flux Xu @fluxxu */ /* A note about the environment that this code runs in... assumed globals: - `module` (from Node.js module system and webpack) assumed in scope after injection into the Elm IIFE: - `scope` (has an 'Elm' property which contains the public Elm API) - various functions defined by Elm which we have to hook such as `_Platform_initialize` and `_Scheduler_binding` */ if (module.hot) { (function () { "use strict"; //polyfill for IE: https://github.com/fluxxu/elm-hot-loader/issues/16 if (typeof Object.assign != 'function') { Object.assign = function (target) { 'use strict'; if (target == null) { throw new TypeError('Cannot convert undefined or null to object'); } target = Object(target); for (var index = 1; index < arguments.length; index++) { var source = arguments[index]; if (source != null) { for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } } return target; }; } var instances = module.hot.data ? module.hot.data.instances || {} : {}; var uid = module.hot.data ? module.hot.data.uid || 0 : 0; if (Object.keys(instances).length === 0) { console.log("[elm-hot] Enabled"); } var cancellers = []; // These 2 variables act as dynamically-scoped variables which are set only when the // Elm module's hooked init function is called. var initializingInstance = null; var swappingInstance = null; module.hot.accept(); module.hot.dispose(function (data) { data.instances = instances; data.uid = uid; // Cleanup pending async tasks // First, make sure that no new tasks can be started until we finish replacing the code _Scheduler_binding = function () { return _Scheduler_fail(new Error('[elm-hot] Inactive Elm instance.')) }; // Second, kill pending tasks belonging to the old instance if (cancellers.length) { console.log('[elm-hot] Killing ' + cancellers.length + ' running processes...'); try { cancellers.forEach(function (cancel) { cancel(); }); } catch (e) { console.warn('[elm-hot] Kill process error: ' + e.message); } } }); function getId() { return ++uid; } function findPublicModules(parent, path) { var modules = []; for (var key in parent) { var child = parent[key]; var currentPath = path ? path + '.' + key : key; if ('init' in child) { modules.push({ path: currentPath, module: child }); } else { modules = modules.concat(findPublicModules(child, currentPath)); } } return modules; } function registerInstance(domNode, flags, path, portSubscribes, portSends) { var id = getId(); var instance = { id: id, path: path, domNode: domNode, flags: flags, portSubscribes: portSubscribes, portSends: portSends, navKeyPath: null, // array of JS property names by which the Browser.Navigation.Key can be found in the model lastState: null // last Elm app state (root model) }; return instances[id] = instance } function isFullscreenApp() { // Returns true if the Elm app will take over the entire DOM body. return typeof elm$browser$Browser$application !== 'undefined' || typeof elm$browser$Browser$document !== 'undefined'; } function wrapDomNode(node) { // When embedding an Elm app into a specific DOM node, Elm will replace the provided // DOM node with the Elm app's content. When the Elm app is compiled normally, the // original DOM node is reused (its attributes and content changes, but the object // in memory remains the same). But when compiled using `--debug`, Elm will completely // destroy the original DOM node and instead replace it with 2 brand new nodes: one // for your Elm app's content and the other for the Elm debugger UI. In this case, // if you held a reference to the DOM node provided for embedding, it would be orphaned // after Elm module initialization. // // So in order to make both cases consistent and isolate us from changes in how Elm // does this, we will insert a dummy node to wrap the node for embedding and hold // a reference to the dummy node. // // We will also put a tag on the dummy node so that the Elm developer knows who went // behind their back and rudely put stuff in their DOM. var dummyNode = document.createElement("div"); dummyNode.setAttribute("data-elm-hot", "true"); var parentNode = node.parentNode; parentNode.replaceChild(dummyNode, node); dummyNode.appendChild(node); return dummyNode; } function wrapPublicModule(path, module) { var originalInit = module.init; if (originalInit) { module.init = function (args) { var elm; var portSubscribes = {}; var portSends = {}; var domNode = null; var flags = null; if (typeof args !== 'undefined') { // normal case domNode = args['node'] && !isFullscreenApp() ? wrapDomNode(args['node']) : document.body; flags = args['flags']; } else { // rare case: Elm allows init to be called without any arguments at all domNode = document.body; flags = undefined } initializingInstance = registerInstance(domNode, flags, path, portSubscribes, portSends); elm = originalInit(args); wrapPorts(elm, portSubscribes, portSends); initializingInstance = null; return elm; }; } else { console.error("Could not find a public module to wrap at path " + path) } } function swap(Elm, instance) { console.log('[elm-hot] Hot-swapping module: ' + instance.path); swappingInstance = instance; // remove from the DOM everything that had been created by the old Elm app var containerNode = instance.domNode; while (containerNode.lastChild) { containerNode.removeChild(containerNode.lastChild); } var m = getAt(instance.path.split('.'), Elm); var elm; if (m) { // prepare to initialize the new Elm module var args = {flags: instance.flags}; if (containerNode === document.body) { // fullscreen case: no additional args needed } else { // embed case: provide a new node for Elm to use var nodeForEmbed = document.createElement("div"); containerNode.appendChild(nodeForEmbed); args['node'] = nodeForEmbed; } elm = m.init(args); Object.keys(instance.portSubscribes).forEach(function (portName) { if (portName in elm.ports && 'subscribe' in elm.ports[portName]) { var handlers = instance.portSubscribes[portName]; if (!handlers.length) { return; } console.log('[elm-hot] Reconnect ' + handlers.length + ' handler(s) to port \'' + portName + '\' (' + instance.path + ').'); handlers.forEach(function (handler) { elm.ports[portName].subscribe(handler); }); } else { delete instance.portSubscribes[portName]; console.log('[elm-hot] Port was removed: ' + portName); } }); Object.keys(instance.portSends).forEach(function (portName) { if (portName in elm.ports && 'send' in elm.ports[portName]) { console.log('[elm-hot] Replace old port send with the new send'); instance.portSends[portName] = elm.ports[portName].send; } else { delete instance.portSends[portName]; console.log('[elm-hot] Port was removed: ' + portName); } }); } else { console.log('[elm-hot] Module was removed: ' + instance.path); } swappingInstance = null; } function wrapPorts(elm, portSubscribes, portSends) { var portNames = Object.keys(elm.ports || {}); //hook ports if (portNames.length) { // hook outgoing ports portNames .filter(function (name) { return 'subscribe' in elm.ports[name]; }) .forEach(function (portName) { var port = elm.ports[portName]; var subscribe = port.subscribe; var unsubscribe = port.unsubscribe; elm.ports[portName] = Object.assign(port, { subscribe: function (handler) { console.log('[elm-hot] ports.' + portName + '.subscribe called.'); if (!portSubscribes[portName]) { portSubscribes[portName] = [handler]; } else { //TODO handle subscribing to single handler more than once? portSubscribes[portName].push(handler); } return subscribe.call(port, handler); }, unsubscribe: function (handler) { console.log('[elm-hot] ports.' + portName + '.unsubscribe called.'); var list = portSubscribes[portName]; if (list && list.indexOf(handler) !== -1) { list.splice(list.lastIndexOf(handler), 1); } else { console.warn('[elm-hot] ports.' + portName + '.unsubscribe: handler not subscribed'); } return unsubscribe.call(port, handler); } }); }); // hook incoming ports portNames .filter(function (name) { return 'send' in elm.ports[name]; }) .forEach(function (portName) { var port = elm.ports[portName]; portSends[portName] = port.send; elm.ports[portName] = Object.assign(port, { send: function (val) { return portSends[portName].call(port, val); } }); }); } return portSubscribes; } /* Breadth-first search for a `Browser.Navigation.Key` in the user's app model. Returns the key and keypath or null if not found. */ function findNavKey(rootModel) { var queue = []; if (isDebuggerModel(rootModel)) { /* Extract the user's app model from the Elm Debugger's model. The Elm debugger can hold multiple references to the user's model (e.g. in its "history"). So we must be careful to only search within the "state" part of the Debugger. */ queue.push({value: rootModel['state'], keypath: ['state']}); } else { queue.push({value: rootModel, keypath: []}); } while (queue.length !== 0) { var item = queue.shift(); // The nav key is identified by a runtime tag added by the elm-hot injector. if (item.value.hasOwnProperty("elm-hot-nav-key")) { // found it! return item; } if (typeof item.value !== "object") { continue; } for (var propName in item.value) { if (!item.value.hasOwnProperty(propName)) continue; var newKeypath = item.keypath.slice(); newKeypath.push(propName); queue.push({value: item.value[propName], keypath: newKeypath}) } } return null; } function isDebuggerModel(model) { return model && model.hasOwnProperty("expando") && model.hasOwnProperty("state"); } function getAt(keyPath, obj) { return keyPath.reduce(function (xs, x) { return (xs && xs[x]) ? xs[x] : null }, obj) } function removeNavKeyListeners(navKey) { window.removeEventListener('popstate', navKey.value); window.navigator.userAgent.indexOf('Trident') < 0 || window.removeEventListener('hashchange', navKey.value); } // hook program creation var initialize = _Platform_initialize; _Platform_initialize = function (flagDecoder, args, init, update, subscriptions, stepperBuilder) { var instance = initializingInstance || swappingInstance; var tryFirstRender = !!swappingInstance; var hookedInit = function (args) { var initialStateTuple = init(args); if (swappingInstance) { var oldModel = swappingInstance.lastState; var newModel = initialStateTuple.a; if (typeof elm$browser$Browser$application !== 'undefined') { // attempt to find the Browser.Navigation.Key in the newly-constructed model // and bring it along with the rest of the old data. var newKeyLoc = findNavKey(newModel); var error = null; if (newKeyLoc === null) { error = "could not find Browser.Navigation.Key in the new app model"; } else if (instance.navKeyPath === null) { error = "could not find Browser.Navigation.Key in the old app model."; } else if (newKeyLoc.keypath.toString() !== instance.navKeyPath.toString()) { error = "the location of the Browser.Navigation.Key in the model has changed."; } else { var oldNavKey = getAt(instance.navKeyPath, oldModel); if (oldNavKey === null) { error = "keypath " + instance.navKeyPath + " is invalid. Please report a bug." } else { // remove event listeners attached to the old nav key removeNavKeyListeners(oldNavKey); // insert the new nav key into the old model in the exact same location var parentKeyPath = newKeyLoc.keypath.slice(0, -1); var lastSegment = newKeyLoc.keypath.slice(-1)[0]; var oldParent = getAt(parentKeyPath, oldModel); oldParent[lastSegment] = newKeyLoc.value; } } if (error !== null) { console.error("[elm-hot] Hot-swapping " + instance.path + " not possible: " + error); oldModel = newModel; } } // the heart of the app state hot-swap initialStateTuple.a = oldModel; // ignore any Cmds returned by the init during hot-swap initialStateTuple.b = elm$core$Platform$Cmd$none; } else { // capture the initial state for later initializingInstance.lastState = initialStateTuple.a; // capture Browser.application's navigation key for later if (typeof elm$browser$Browser$application !== 'undefined') { var navKeyLoc = findNavKey(initializingInstance.lastState); if (!navKeyLoc) { console.error("[elm-hot] Hot-swapping disabled for " + instance.path + ": could not find Browser.Navigation.Key in your model."); instance.navKeyPath = null; } else { instance.navKeyPath = navKeyLoc.keypath; } } } return initialStateTuple }; var hookedStepperBuilder = function (sendToApp, model) { var result; // first render may fail if shape of model changed too much if (tryFirstRender) { tryFirstRender = false; try { result = stepperBuilder(sendToApp, model) } catch (e) { throw new Error('[elm-hot] Hot-swapping ' + instance.path + ' is not possible, please reload page. Error: ' + e.message) } } else { result = stepperBuilder(sendToApp, model) } return function (nextModel, isSync) { if (instance) { // capture the state after every step so that later we can restore from it during a hot-swap instance.lastState = nextModel } return result(nextModel, isSync) } }; return initialize(flagDecoder, args, hookedInit, update, subscriptions, hookedStepperBuilder) }; // hook process creation var originalBinding = _Scheduler_binding; _Scheduler_binding = function (originalCallback) { return originalBinding(function () { // start the scheduled process, which may return a cancellation function. var cancel = originalCallback.apply(this, arguments); if (cancel) { cancellers.push(cancel); return function () { cancellers.splice(cancellers.indexOf(cancel), 1); return cancel(); }; } return cancel; }); }; scope['_elm_hot_loader_init'] = function (Elm) { // swap instances var removedInstances = []; for (var id in instances) { var instance = instances[id]; if (instance.domNode.parentNode) { swap(Elm, instance); } else { removedInstances.push(id); } } removedInstances.forEach(function (id) { delete instance[id]; }); // wrap all public modules var publicModules = findPublicModules(Elm); publicModules.forEach(function (m) { wrapPublicModule(m.path, m.module); }); } })(); scope['_elm_hot_loader_init'](scope['Elm']); } //////////////////// HMR END ////////////////////