From 43f9b2921911a0a28a2f7e038baff7f94076640d Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sun, 10 Aug 2025 02:19:31 +0200 Subject: [PATCH 1/6] New injections for forked elm packages --- extra/Lamdera/Injection.hs | 524 ++++--------------------------------- 1 file changed, 47 insertions(+), 477 deletions(-) diff --git a/extra/Lamdera/Injection.hs b/extra/Lamdera/Injection.hs index 373c7ad8a..310181dda 100644 --- a/extra/Lamdera/Injection.hs +++ b/extra/Lamdera/Injection.hs @@ -236,71 +236,14 @@ injections outputType mode = & Text.replace "// equals override injection marker" equalsOverride in case outputType of - -- NotLamdera was added when we fixed the hot loading of a new app version in the browser. - -- The frontend version of the injections – which used to be what you got when using the - -- lamdera compiler for non-lamdera things – then became much more complicated. - -- In order not to break elm-pages (which calls `app.die()`) we preserved the old frontend - -- injections. Note that the `.die()` method is incomplete: It does not kill all apps completely - -- and does not help the garbage collector enough for a killed app to be fully garbage collected. NotLamdera -> [text| $lamderaContainersExtensions_ - - function _Platform_initialize(flagDecoder, args, init, update, subscriptions, stepperBuilder) - { - var result = A2(_Json_run, flagDecoder, _Json_wrap(args ? args['flags'] : undefined)); - - // @TODO need to figure out how to get this to automatically escape by mode? - //$$elm$$core$$Result$$isOk(result) || _Debug_crash(2 /**/, _Json_errorToString(result.a) /**/); - $$elm$$core$$Result$$isOk(result) || _Debug_crash(2 /**_UNUSED/, _Json_errorToString(result.a) /**/); - - var managers = {}; - var initPair = init(result.a); - var model = (args && args['model']) || initPair.a; - - var stepper = stepperBuilder(sendToApp, model); - var ports = _Platform_setupEffects(managers, sendToApp); - - var upgradeMode = false; - - function sendToApp(msg, viewMetadata) - { - if (upgradeMode) { - // No more messages should run in upgrade mode - _Platform_enqueueEffects(managers, $$elm$$core$$Platform$$Cmd$$none, $$elm$$core$$Platform$$Sub$$none); - return; - } - - var pair = A2(update, msg, model); - stepper(model = pair.a, viewMetadata); - _Platform_enqueueEffects(managers, pair.b, subscriptions(model)); - } - - if ((args && args['model']) === undefined) { - _Platform_enqueueEffects(managers, initPair.b, subscriptions(model)); - } - - const die = function() { - // Stop all subscriptions. - // This must be done before clearing the stuff below. - _Platform_enqueueEffects(managers, _Platform_batch(_List_Nil), _Platform_batch(_List_Nil)); - - managers = null; - model = null; - stepper = null; - ports = null; - _Platform_effectsQueue = []; - } - - return ports ? { - ports: ports, - gm: function() { return model }, - eum: function() { upgradeMode = true }, - die: die, - fns: {} - } : {}; - } + + function _Lamdera_inject(app) { + app.die = app.stop; + } |] LamderaBackend -> @@ -308,445 +251,72 @@ injections outputType mode = $lamderaContainersExtensions_ - function _Platform_initialize(flagDecoder, args, init, update, subscriptions, stepperBuilder) - { - var result = A2(_Json_run, flagDecoder, _Json_wrap(args ? args['flags'] : undefined)); - - // @TODO need to figure out how to get this to automatically escape by mode? - //$$elm$$core$$Result$$isOk(result) || _Debug_crash(2 /**/, _Json_errorToString(result.a) /**/); - $$elm$$core$$Result$$isOk(result) || _Debug_crash(2 /**_UNUSED/, _Json_errorToString(result.a) /**/); - - var managers = {}; - var initPair = init(result.a); - var model = (args && args['model']) || initPair.a; - - var stepper = stepperBuilder(sendToApp, model); - var ports = _Platform_setupEffects(managers, sendToApp); - - var upgradeMode = false; - - function sendToApp(msg, viewMetadata) - { - if (upgradeMode) { - // No more messages should run in upgrade mode - _Platform_enqueueEffects(managers, $$elm$$core$$Platform$$Cmd$$none, $$elm$$core$$Platform$$Sub$$none); - return; - } - - stepper(model = pair.a, viewMetadata); - _Platform_enqueueEffects(managers, pair.b, subscriptions(model)); - } - - if ((args && args['model']) === undefined) { - _Platform_enqueueEffects(managers, initPair.b, subscriptions(model)); - } - - const die = function() { - // Stop all subscriptions. - // This must be done before clearing the stuff below. - _Platform_enqueueEffects(managers, _Platform_batch(_List_Nil), _Platform_batch(_List_Nil)); - - managers = null; - model = null; - stepper = null; - ports = null; - _Platform_effectsQueue = []; - } - - return ports ? { - ports: ports, - gm: function() { return model }, - eum: function() { upgradeMode = true }, - die: die, - fns: {} - } : {}; - } - var isLamderaRuntime = typeof isLamdera !== 'undefined'; - function _Platform_initialize(flagDecoder, args, init, update, subscriptions, stepperBuilder) - { - var result = A2(_Json_run, flagDecoder, _Json_wrap(args ? args['flags'] : undefined)); - - // @TODO need to figure out how to get this to automatically escape by mode? - //$$elm$$core$$Result$$isOk(result) || _Debug_crash(2 /**/, _Json_errorToString(result.a) /**/); - $$elm$$core$$Result$$isOk(result) || _Debug_crash(2 /**_UNUSED/, _Json_errorToString(result.a) /**/); - + function _Lamdera_inject(app, callUpdate, model) { + app.die = app.stop; - var managers = {}; - var initPair = init(result.a); - var model = (args && args['model']) || initPair.a; - - var stepper = stepperBuilder(sendToApp, model); - var ports = _Platform_setupEffects(managers, sendToApp); - - var pos = 0; + app.fns = + { decodeWirePayloadHeader: $$author$$project$$LamderaHelpers$$decodeWirePayloadHeader + , decodeWireAnalytics: $$author$$project$$LamderaHelpers$$decodeWireAnalytics + , getUserModel : function() { return model.userModel } + }; - function mtime() { // microseconds - if (!isLamderaRuntime) { return 0; } - const hrTime = process.hrtime(); - return Math.floor(hrTime[0] * 1000000 + hrTime[1] / 1000); - } + var pos = 0; - var buriedTimestamp = null; + function mtime() { // microseconds + if (!isLamderaRuntime) { return 0; } + const hrTime = process.hrtime(); + return Math.floor(hrTime[0] * 1000000 + hrTime[1] / 1000); + } - function sendToApp(msg, viewMetadata) - { - if (buriedTimestamp !== null) { - const elapsed = Date.now() - buriedTimestamp; - // This tries to turn `HomePageMsg (WeatherWidgetMsg (WeatherReportReceived WeatherReport))` - // into `"HomePageMsg WeatherWidgetMsg WeatherReportReceived"`. - // The idea is that if the timeout for forwarding messages isn't enough, we want to know what - // message somebody used that took even longer, but without reporting the entire msg. - // Note that in `--optimize` mode, the above string would become something like `"1 3 6"`, - // but it's better than nothing. - let msgName = '(unknown message)'; - if (msg.$) { - msgName = msg.$; - let current = msg; - while (current.a && current.a.$ && !current.b) { - current = current.a; - msgName = msgName + ' ' + current.$; - } - } - bugsnag.notify(new Error('Got message ' + elapsed + ' ms after app was buried: ' + msgName)); - return; - } + callUpdate.call = function(update, msg, model) { + var serializeDuration, logDuration = null; + var start = mtime(); - var serializeDuration, logDuration = null; - var start = mtime(); + var pair = A2(update, msg, model); - var pair = A2(update, msg, model); + const updateDuration = mtime() - start; + start = mtime(); - const updateDuration = mtime() - start; + if (isLamderaRuntime && loggingEnabled) { + pos = pos + 1; + const s = $$author$$project$$LBR$$serialize(msg); + serializeDuration = mtime() - start; start = mtime(); - - if (isLamderaRuntime && loggingEnabled) { - pos = pos + 1; - const s = $$author$$project$$LBR$$serialize(msg); - serializeDuration = mtime() - start; - start = mtime(); - insertEvent(pos, global.config.version, s.a, updateDuration, serializeDuration, A2($$elm$$core$$Maybe$$withDefault, null, s.b)); - logDuration = mtime() - start; - } - - stepper(model = pair.a, viewMetadata); - _Platform_enqueueEffects(managers, pair.b, subscriptions(model)); - } - - if ((args && args['model']) === undefined) { - _Platform_enqueueEffects(managers, initPair.b, subscriptions(model)); - } - - var fns = - { decodeWirePayloadHeader: $$author$$project$$LamderaHelpers$$decodeWirePayloadHeader - , decodeWireAnalytics: $$author$$project$$LamderaHelpers$$decodeWireAnalytics - , getUserModel : function() { return model.userModel } - } - - const die = function() { - // In case there still are any pending commands, setting this flag means - // that nothing happens when they finish. - buriedTimestamp = Date.now(); - - // The app won't be garbage collected until all pending commands are done. - // We can reclaim most memory immediately by manually clearing the model early. - model = null; - - // On the frontend, we have to clear the effect managers, since they prevent sendToApp from being GC:ed, - // which prevents the whole app from being GC:ed. On the backend, it does not seem to. - // We still do it here for consistency (it doesn't hurt). - _Platform_effectManagers = {}; + insertEvent(pos, global.config.version, s.a, updateDuration, serializeDuration, A2($$elm$$core$$Maybe$$withDefault, null, s.b)); + logDuration = mtime() - start; } - // On the frontend, clearing args helps garbage collection. On the backend, it does not seem to. - // We still do it here for consistency (it doesn't hurt). - args = null; - - return ports ? { - ports: ports, - die: die, - fns: fns - } : {}; - } + model = pair.a; + return pair; + }; + } |] - -- LamderaFrontend or LamderaLive - _ -> - let - shouldProxy = - onlyIf (outputType == LamderaLive) - [text| - shouldProxy = $$author$$project$$LocalDev$$shouldProxy(msg) - |] - in + LamderaFrontend -> [text| $lamderaContainersExtensions_ - function _Platform_initialize(flagDecoder, args, init, update, subscriptions, stepperBuilder) - { - var result = A2(_Json_run, flagDecoder, _Json_wrap(args ? args['flags'] : undefined)); - - // @TODO need to figure out how to get this to automatically escape by mode? - //$$elm$$core$$Result$$isOk(result) || _Debug_crash(2 /**/, _Json_errorToString(result.a) /**/); - $$elm$$core$$Result$$isOk(result) || _Debug_crash(2 /**_UNUSED/, _Json_errorToString(result.a) /**/); - - var managers = {}; - var initPair = init(result.a); - var model = initPair.a; - - // We'll temporarily overwrite these variables or functions. - var F2_backup = F2; - var _Browser_window_backup = _Browser_window; - var _VirtualDom_virtualize_backup = _VirtualDom_virtualize; - var _VirtualDom_applyPatches_backup = _VirtualDom_applyPatches; - var _VirtualDom_equalEvents_backup = _VirtualDom_equalEvents; - - // stepperBuilder calls impl.setup() (if it exists, which it does only for - // Browser.application) as the first thing it does. impl.setup() returns the - // divertHrefToApp function, which is used to create the event listener for - // all elements. That divertHrefToApp function is constructed using F2. - // Here we override F2 to store the listener on the DOM node so we can - // remove it later. _VirtualDom_virtualize is called a couple of lines - // later, so we use that to restore the original F2 function, so it can do - // the right thing when the view function is called. - F2 = function(f) { - return function(domNode) { - var listener = function(event) { - return f(domNode, event); - }; - domNode.elmAf = listener; - return listener; - }; - }; - - // To get hold of the Browser.Navigation.key, and to be able to remove the popstate and hashchange listeners. - _Browser_window = { - navigator: _Browser_window_backup.navigator, - addEventListener: function(eventName, listener) { - _Browser_navKey = listener; - _Browser_window_backup.addEventListener(eventName, listener); - }, - }; - - // When passing in the last rendered VNode from a previous app: - if (args && args.vn) { - // Instead of virtualizing the existing DOM into a VNode, just use the - // one from the previous app. Html.map messes up Elm's - // _VirtualDom_virtualize, causing the entire thing inside the Html.map - // to be re-created even though it is already the correct DOM. - _VirtualDom_virtualize = function() { - F2 = F2_backup; // Restore F2 as mentioned above. - return args.vn; - }; - - _VirtualDom_applyPatches = function(rootDomNode, oldVirtualNode, patches, eventNode) { - if (patches.length !== 0) { - _VirtualDom_addDomNodes(rootDomNode, oldVirtualNode, patches, eventNode); - } - _VirtualDom_lastDomNode = _VirtualDom_applyPatchesHelp(rootDomNode, patches); - // Restore the event listeners on the elements: - var aElements = _VirtualDom_lastDomNode.getElementsByTagName('a'); - for (var i = 0; i < aElements.length; i++) { - var domNode = aElements[i]; - domNode.addEventListener('click', _VirtualDom_divertHrefToApp(domNode)); - } - return _VirtualDom_lastDomNode; - } - - // Force all event listeners to be re-applied: - _VirtualDom_equalEvents = function() { - return false; - } - } else { - _VirtualDom_virtualize = function(node) { - F2 = F2_backup; // Restore F2 as mentioned above. - return _VirtualDom_virtualize_backup(node); - }; - } - - var stepper = stepperBuilder(sendToApp, model); - - // Restore the original functions and variables. - F2 = F2_backup; // Should already be restored by now, but just in case. - _Browser_window = _Browser_window_backup; - _VirtualDom_virtualize = _VirtualDom_virtualize_backup; - _VirtualDom_applyPatches = _VirtualDom_applyPatches_backup; - _VirtualDom_equalEvents = _VirtualDom_equalEvents_backup; - - if (args && args.vn) { - // Html.map puts `.elm_event_node_ref = { __tagger: taggers, __parent: parent_elm_event_node_ref }` - // on the DOM node for its first non-Html.map child virtual DOM node. __parent is a reference to - // the .elm_event_node_ref of a parent Html.map further up the tree. Modifying one modifies the other, - // since they are the same object. The top-most Html.map nodes have __parent set to the sendToApp function. - // All the __tagger should be updated now to stuff from the new app, but the __parent sendToApp still points - // to the old app, so we need to update it to the current app. - // This relies on that fact that we do `List.map (Html.map FEMsg) body`. If that weren't the case, we could - // have to crawl the tree recursively to find the top-most Html.map nodes. - for (var i = 0; i < _VirtualDom_lastDomNode.childNodes.length; i++) { - var element = _VirtualDom_lastDomNode.childNodes[i]; - if (element.elm_event_node_ref && typeof element.elm_event_node_ref.p === 'function') { - element.elm_event_node_ref.p = sendToApp; - } - } - } - - var ports = _Platform_setupEffects(managers, sendToApp); - - var buriedTimestamp = null; - - function sendToApp(msg, viewMetadata) - { - if (buriedTimestamp !== null) { - const elapsed = Date.now() - buriedTimestamp; - let msgName = '(unknown message)'; - if (msg.$) { - msgName = msg.$; - let current = msg; - while (current.a && current.a.$ && !current.b) { - current = current.a; - msgName = msgName + ' ' + current.$; - } - } - window.lamdera.bs.notify(new Error('Got message ' + elapsed + ' ms after app was buried: ' + msgName)); - return; - } - - $shouldProxy - - var pair = A2(update, msg, model); - stepper(model = pair.a, viewMetadata); - _Platform_enqueueEffects(managers, pair.b, subscriptions(model)); - } - - _Platform_enqueueEffects(managers, initPair.b, subscriptions(model)); - - // Stops the app from getting more input, and from rendering. - // It doesn't die completely: Already running cmds will still run, and - // hit the update function, which then redirects the messages to the new app. - const die = function() { - // Render one last time, synchronously, in case there is a scheduled - // render with requestAnimationFrame (which then become no-ops). - // Rendering mutates the vdom, and we want those mutations. - stepper(model, true /* isSync */); - - // Remove Elm's event listeners. Both the ones added - // automatically on every element, as well as the ones - // added by using Html.Events. - var elements = _VirtualDom_lastDomNode.getElementsByTagName('*'); - for (var i = 0; i < elements.length; i++) { - var element = elements[i]; - if (element.elmAf) { - element.removeEventListener('click', element.elmAf); - delete element.elmAf; - } - if (element.elmFs) { - for (var key in element.elmFs) { - element.removeEventListener(key, element.elmFs[key]); - } - delete element.elmFs; - } - // Leave element.elm_event_node_ref behind, because the first - // render in the new app crashes otherwise. It contains references - // to the old app, but all of that should go away after the first - // render, and the element.elm_event_node_ref.p stuff above. - } - - // Remove the popstate and hashchange listeners. - if (_Browser_navKey) { - _Browser_window.removeEventListener('popstate', _Browser_navKey); - _Browser_window.removeEventListener('hashchange', _Browser_navKey); - // Remove reference to .a aka .__sendToApp which prevents GC. - delete _Browser_navKey.a; - } - - // Stop rendering: - stepper = function() {}; - - // Note that subscriptions are turned off in Elm (by returning Sub.none) - // in upgrade mode, and the update function stops doing its regular business - // and just forwards messages instead. - - return _VirtualDom_lastVNode; - } - - // This can't be done in the die function, because then it's not possible to - // trigger an outgoing port to redirect messages. This is supposed to be called - // when all pending commands are done. - const bury = function() { - // In case there still are any pending commands, setting this flag means - // that nothing happens when they finish. - buriedTimestamp = Date.now(); - - // The app won't be garbage collected until all pending commands are done. - // We can reclaim most memory immediately by manually clearing the model early. - model = null; - - // Clear effect managers, since they prevent sendToApp from being GC:ed, - // which prevents the whole app from being GC:ed. - _Platform_effectManagers = {}; - }; - - // Clearing args means the flags (like the passed in model) can be GC:ed (in the new app). - args = null; + function _Lamdera_inject(app) { + app.die = app.stop; + } + |] - return ports ? { - ports: ports, - die: die, - bury: bury, - } : {}; - } + LamderaLive -> + [text| - // Keep track of the last VNode rendered so we can pass it to the next app later. - var _VirtualDom_lastVNode = null; - function _VirtualDom_diff(x, y) - { - _VirtualDom_lastVNode = y; - var patches = []; - _VirtualDom_diffHelp(x, y, patches, 0); - return patches; - } + $lamderaContainersExtensions_ - // Keep track of the reference to the latest root DOM node so we can perform cleanups in it later. - var _VirtualDom_lastDomNode = null; - function _VirtualDom_applyPatches(rootDomNode, oldVirtualNode, patches, eventNode) - { - if (patches.length === 0) - { - return (_VirtualDom_lastDomNode = rootDomNode); - } + function _Lamdera_inject(app, callUpdate) { + app.die = app.stop; - _VirtualDom_addDomNodes(rootDomNode, oldVirtualNode, patches, eventNode); - return (_VirtualDom_lastDomNode = _VirtualDom_applyPatchesHelp(rootDomNode, patches)); + callUpdate.call = function(update, msg, model) { + shouldProxy = $$author$$project$$LocalDev$$shouldProxy(msg); + return A2(update, msg, model); + }; } - - // In Elm, Browser.Navigation.Key is a function behind the scenes. It is passed and called here. - // In Lamdera, the Key becomes an object after a Wire roundtrip, so we just take the key as a "password" - // but then call the actual function ourselves. - var _Browser_navKey = null; - var _Browser_go = F2(function(key, n) { - return A2($$elm$$core$$Task$$perform, $$elm$$core$$Basics$$never, _Scheduler_binding(function() { - n && history.go(n); - _Browser_navKey(); - })); - }); - // $$elm$$browser$$Browser$$Navigation$$back is not a direct assignment so it does not need to be replaced. - var $$elm$$browser$$Browser$$Navigation$$forward = _Browser_go; - var _Browser_pushUrl = F2(function(key, url) { - return A2($$elm$$core$$Task$$perform, $$elm$$core$$Basics$$never, _Scheduler_binding(function() { - history.pushState({}, "", url); - _Browser_navKey(); - })); - }); - var $$elm$$browser$$Browser$$Navigation$$pushUrl = _Browser_pushUrl; - var _Browser_replaceUrl = F2(function(key, url) { - return A2($$elm$$core$$Task$$perform, $$elm$$core$$Basics$$never, _Scheduler_binding(function() { - history.replaceState({}, "", url); - _Browser_navKey(); - })); - }); - var $$elm$$browser$$Browser$$Navigation$$replaceUrl = _Browser_replaceUrl; |] -- // https://github.com/elm/bytes/issues/20 From 99c307e1f1e48bc3313fc0f5b29605edb33cede3 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sun, 10 Aug 2025 03:11:17 +0200 Subject: [PATCH 2/6] Remove lamdera/containers injection --- extra/Lamdera/Injection.hs | 83 -------- .../lamdera-containers-extensions.js | 185 ------------------ 2 files changed, 268 deletions(-) delete mode 100644 extra/Lamdera/Injection/lamdera-containers-extensions.js diff --git a/extra/Lamdera/Injection.hs b/extra/Lamdera/Injection.hs index 310181dda..7084ba640 100644 --- a/extra/Lamdera/Injection.hs +++ b/extra/Lamdera/Injection.hs @@ -171,76 +171,10 @@ source mode mains = injections :: OutputType -> Mode.Mode -> Text injections outputType mode = - let - previousVersionInt = - -- @TODO maybe its time to consolidate the global config... - (unsafePerformIO $ lookupEnv "VERSION") - & maybe "0" id - & read - & subtract 1 - - previousVersion = show_ previousVersionInt - - {-| This code overrides how == is handled in Elm for SeqDict and SeqSet. - The code for handling Dict and Set is also injected here though the behavior is unchanged. - - The entire == function kernel code is overriden further down* and this is inserted into it, - but the rest of the equals function kernel code is the same regardless of whether --optimize is used or not so - it was cleaner to not write it all twice for --optimize and non--optimize. - - *like with Dict and Set, the rest of the equals kernel code behavior is unchanged. - -} - equalsOverride = - if isOptimizedMode mode then - [text| - if (x.$$ < 0) - { - if (x.$$ < -10) - { - x = $$lamdera$$containers$$SeqDict$$toList(x); - y = $$lamdera$$containers$$SeqDict$$toList(y); - } - else - { - x = $$elm$$core$$Dict$$toList(x); - y = $$elm$$core$$Dict$$toList(y); - } - } - |] - else - [text| - if (x.$$ === 'Set_elm_builtin') - { - x = $$elm$$core$$Set$$toList(x); - y = $$elm$$core$$Set$$toList(y); - } - if (x.$$ === 'RBNode_elm_builtin' || x.$$ === 'RBEmpty_elm_builtin') - { - x = $$elm$$core$$Dict$$toList(x); - y = $$elm$$core$$Dict$$toList(y); - } - if (x.$$ === 'SeqDict_elm_builtin') - { - x = $$lamdera$$containers$$SeqDict$$toList(x); - y = $$lamdera$$containers$$SeqDict$$toList(y); - } - if (x.$$ === 'SeqSet_elm_builtin') - { - x = $$lamdera$$containers$$SeqSet$$toList(x); - y = $$lamdera$$containers$$SeqSet$$toList(y); - } - |] - - lamderaContainersExtensions_ = - Ext.Common.bsToText lamderaContainersExtensions - & Text.replace "// equals override injection marker" equalsOverride - in case outputType of NotLamdera -> [text| - $lamderaContainersExtensions_ - function _Lamdera_inject(app) { app.die = app.stop; } @@ -249,8 +183,6 @@ injections outputType mode = LamderaBackend -> [text| - $lamderaContainersExtensions_ - var isLamderaRuntime = typeof isLamdera !== 'undefined'; function _Lamdera_inject(app, callUpdate, model) { @@ -297,8 +229,6 @@ injections outputType mode = LamderaFrontend -> [text| - $lamderaContainersExtensions_ - function _Lamdera_inject(app) { app.die = app.stop; } @@ -307,8 +237,6 @@ injections outputType mode = LamderaLive -> [text| - $lamderaContainersExtensions_ - function _Lamdera_inject(app, callUpdate) { app.die = app.stop; @@ -497,14 +425,3 @@ mainsInclude list mains = else False _ -> False - -{-| - Overrides to the following functions to add support for lamdera/containers SeqDict and SeqSet, - displaying as `SeqDict.fromList [ ... ]` and `SeqSet.fromList [ ... ]` - - _Debug_toAnsiString - _Utils_eqHelp --} -lamderaContainersExtensions :: BS.ByteString -lamderaContainersExtensions = - $(bsToExp =<< runIO (Lamdera.Relative.readByteString "extra/Lamdera/Injection/lamdera-containers-extensions.js")) diff --git a/extra/Lamdera/Injection/lamdera-containers-extensions.js b/extra/Lamdera/Injection/lamdera-containers-extensions.js deleted file mode 100644 index d3039ffd3..000000000 --- a/extra/Lamdera/Injection/lamdera-containers-extensions.js +++ /dev/null @@ -1,185 +0,0 @@ -function _Debug_toAnsiString(ansi, value) -{ - if (typeof value === 'function') - { - return _Debug_internalColor(ansi, ''); - } - - if (typeof value === 'boolean') - { - return _Debug_ctorColor(ansi, value ? 'True' : 'False'); - } - - if (typeof value === 'number') - { - return _Debug_numberColor(ansi, value + ''); - } - - if (value instanceof String) - { - return _Debug_charColor(ansi, "'" + _Debug_addSlashes(value, true) + "'"); - } - - if (typeof value === 'string') - { - return _Debug_stringColor(ansi, '"' + _Debug_addSlashes(value, false) + '"'); - } - - if (typeof value === 'object' && '$' in value) - { - var tag = value.$; - - if (typeof tag === 'number') - { - return _Debug_internalColor(ansi, ''); - } - - if (tag[0] === '#') - { - var output = []; - for (var k in value) - { - if (k === '$') continue; - output.push(_Debug_toAnsiString(ansi, value[k])); - } - return '(' + output.join(',') + ')'; - } - - if (tag === 'Set_elm_builtin') - { - return _Debug_ctorColor(ansi, 'Set') - + _Debug_fadeColor(ansi, '.fromList') + ' ' - + _Debug_toAnsiString(ansi, $elm$core$Set$toList(value)); - } - - if (tag === 'RBNode_elm_builtin' || tag === 'RBEmpty_elm_builtin') - { - return _Debug_ctorColor(ansi, 'Dict') - + _Debug_fadeColor(ansi, '.fromList') + ' ' - + _Debug_toAnsiString(ansi, $elm$core$Dict$toList(value)); - } - - if (tag === 'SeqSet_elm_builtin') - { - return _Debug_ctorColor(ansi, 'SeqSet') - + _Debug_fadeColor(ansi, '.fromList') + ' ' - + _Debug_toAnsiString(ansi, $lamdera$containers$SeqSet$toList(value)); - } - - if (tag === 'SeqDict_elm_builtin') - { - return _Debug_ctorColor(ansi, 'SeqDict') - + _Debug_fadeColor(ansi, '.fromList') + ' ' - + _Debug_toAnsiString(ansi, $lamdera$containers$SeqDict$toList(value)); - } - - if (tag === 'Array_elm_builtin') - { - return _Debug_ctorColor(ansi, 'Array') - + _Debug_fadeColor(ansi, '.fromList') + ' ' - + _Debug_toAnsiString(ansi, $elm$core$Array$toList(value)); - } - - if (tag === '::' || tag === '[]') - { - var output = '['; - - value.b && (output += _Debug_toAnsiString(ansi, value.a), value = value.b) - - for (; value.b; value = value.b) // WHILE_CONS - { - output += ',' + _Debug_toAnsiString(ansi, value.a); - } - return output + ']'; - } - - var output = ''; - for (var i in value) - { - if (i === '$') continue; - var str = _Debug_toAnsiString(ansi, value[i]); - var c0 = str[0]; - var parenless = c0 === '{' || c0 === '(' || c0 === '[' || c0 === '<' || c0 === '"' || str.indexOf(' ') < 0; - output += ' ' + (parenless ? str : '(' + str + ')'); - } - return _Debug_ctorColor(ansi, tag) + output; - } - - if (typeof DataView === 'function' && value instanceof DataView) - { - return _Debug_stringColor(ansi, '<' + value.byteLength + ' bytes>'); - } - - if (typeof File !== 'undefined' && value instanceof File) - { - return _Debug_internalColor(ansi, '<' + value.name + '>'); - } - - if (typeof value === 'object') - { - var output = []; - for (var key in value) - { - var field = key[0] === '_' ? key.slice(1) : key; - output.push(_Debug_fadeColor(ansi, field) + ' = ' + _Debug_toAnsiString(ansi, value[key])); - } - if (output.length === 0) - { - return '{}'; - } - return '{ ' + output.join(', ') + ' }'; - } - - return _Debug_internalColor(ansi, ''); -} - -function _Debug_addSlashes(str, isChar) -{ - var s = str - .replace(/\\/g, '\\\\') - .replace(/\n/g, '\\n') - .replace(/\t/g, '\\t') - .replace(/\r/g, '\\r') - .replace(/\v/g, '\\v') - .replace(/\0/g, '\\0'); - - if (isChar) - { - return s.replace(/\'/g, '\\\''); - } - else - { - return s.replace(/\"/g, '\\"'); - } -} - -function _Utils_eqHelp(x, y, depth, stack) -{ - if (x === y) - { - return true; - } - - if (typeof x !== 'object' || x === null || y === null) - { - typeof x === 'function' && $elm$core$Debug$crash(5); - return false; - } - - if (depth > 100) - { - stack.push(_Utils_Tuple2(x,y)); - return true; - } - - // equals override injection marker - - for (var key in x) - { - if (!_Utils_eqHelp(x[key], y[key], depth + 1, stack)) - { - return false; - } - } - return true; -} From 329e5aae593d5e4e543078d021cc7295bb6ee485 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sun, 10 Aug 2025 03:20:56 +0200 Subject: [PATCH 3/6] Add Browser.Navigation.Key support for serialization and migration like File.File --- extra/Lamdera/Evergreen/Snapshot.hs | 1 + extra/Lamdera/TypeHash.hs | 6 ++++++ extra/Lamdera/Wire3/Decoder.hs | 3 ++- extra/Lamdera/Wire3/Encoder.hs | 2 ++ extra/Lamdera/Wire3/Helpers.hs | 1 + 5 files changed, 12 insertions(+), 1 deletion(-) diff --git a/extra/Lamdera/Evergreen/Snapshot.hs b/extra/Lamdera/Evergreen/Snapshot.hs index 0c54d98f9..312fd79f6 100644 --- a/extra/Lamdera/Evergreen/Snapshot.hs +++ b/extra/Lamdera/Evergreen/Snapshot.hs @@ -648,6 +648,7 @@ canonicalToFt version scope interfaces recursionSet canonical tvarMap = -- Kernel concessions for Frontend Model and Msg + ("elm", "browser", "Browser.Navigation", "Key") -> ("Browser.Navigation.Key", Set.singleton moduleName, Map.empty) ("elm", "file", "File", "File") -> ("File.File", Set.singleton moduleName, Map.empty) ("elm-explorations", "webgl", "WebGL.Texture", "Texture") -> ("WebGL.Texture.Texture", Set.singleton moduleName, Map.empty) diff --git a/extra/Lamdera/TypeHash.hs b/extra/Lamdera/TypeHash.hs index a29f0023e..f1cd0327c 100644 --- a/extra/Lamdera/TypeHash.hs +++ b/extra/Lamdera/TypeHash.hs @@ -388,6 +388,12 @@ canonicalToDiffableType targetName interfaces recursionSet canonical tvarMap = -- Frontend JS Kernel types + ("elm", "browser", "Browser.Navigation", "Key") -> + if targetName `elem` ["FrontendMsg", "FrontendModel"] then + DKernelBrowser "Browser.Navigation.Key" + else + kernelErrorBrowserOnly + ("elm", "file", "File", "File") -> if targetName `elem` ["FrontendMsg", "FrontendModel"] then DKernelBrowser "File.File" diff --git a/extra/Lamdera/Wire3/Decoder.hs b/extra/Lamdera/Wire3/Decoder.hs index b7d056b75..25b564db2 100644 --- a/extra/Lamdera/Wire3/Decoder.hs +++ b/extra/Lamdera/Wire3/Decoder.hs @@ -349,6 +349,7 @@ decoderForType ifaces cname tipe = -- Frontend only JS reference types + TType (Module.Canonical (Name "elm" "browser") "Browser.Navigation") "Key" params -> callDecoder "decodeRef" tipe TType (Module.Canonical (Name "elm" "file") "File") "File" params -> callDecoder "decodeRef" tipe TType (Module.Canonical (Name "elm-explorations" "webgl") "WebGL.Texture") "Texture" params -> callDecoder "decodeRef" tipe @@ -456,4 +457,4 @@ decodeRecord ifaces cname fields = decoderForType ifaces cname field ) ) fields - & foldlPairs (|>) \ No newline at end of file + & foldlPairs (|>) diff --git a/extra/Lamdera/Wire3/Encoder.hs b/extra/Lamdera/Wire3/Encoder.hs index 8786f4bc8..de54b8c1e 100644 --- a/extra/Lamdera/Wire3/Encoder.hs +++ b/extra/Lamdera/Wire3/Encoder.hs @@ -212,6 +212,8 @@ encoderForType depth ifaces cname tipe = -- Frontend only JS reference types + TType (Module.Canonical (Name "elm" "browser") "Browser.Navigation") "Key" _ -> + (a (VarForeign mLamdera_Wire "encodeRef" (Forall Map.empty (TLambda tipe tLamdera_Wire_Encoder)))) TType (Module.Canonical (Name "elm" "file") "File") "File" _ -> (a (VarForeign mLamdera_Wire "encodeRef" (Forall Map.empty (TLambda tipe tLamdera_Wire_Encoder)))) TType (Module.Canonical (Name "elm-explorations" "webgl") "WebGL.Texture") "Texture" _ -> diff --git a/extra/Lamdera/Wire3/Helpers.hs b/extra/Lamdera/Wire3/Helpers.hs index 471143aa2..17b38bd37 100644 --- a/extra/Lamdera/Wire3/Helpers.hs +++ b/extra/Lamdera/Wire3/Helpers.hs @@ -122,6 +122,7 @@ isUnsupportedKernelType tipe = -- JS types we are supporting through JS ref encodings. These serialisations -- CANNOT BE DECODED OUTSIDE OF THE JS SCOPE THEY WERE ENCODED IN! + TType (Module.Canonical (Name "elm" "browser") "Browser.Navigation") "File" _ -> False TType (Module.Canonical (Name "elm" "file") "File") "File" _ -> False TType (Module.Canonical (Name "elm-explorations" "webgl") "WebGL.Texture") "Texture" _ -> False From 2434d1c9c56ccdec58381750b1e6a98b43b7b88a Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sun, 10 Aug 2025 13:11:08 +0200 Subject: [PATCH 4/6] Make Browser.Navigation.Key work --- extra/Lamdera/Evergreen/Snapshot.hs | 1 + extra/Lamdera/Injection.hs | 32 +++++++++++++++++++++++++++++ extra/Lamdera/Wire3/Decoder.hs | 3 ++- extra/Lamdera/Wire3/Encoder.hs | 5 +++-- extra/Lamdera/Wire3/Helpers.hs | 3 ++- 5 files changed, 40 insertions(+), 4 deletions(-) diff --git a/extra/Lamdera/Evergreen/Snapshot.hs b/extra/Lamdera/Evergreen/Snapshot.hs index 312fd79f6..dbbace798 100644 --- a/extra/Lamdera/Evergreen/Snapshot.hs +++ b/extra/Lamdera/Evergreen/Snapshot.hs @@ -648,6 +648,7 @@ canonicalToFt version scope interfaces recursionSet canonical tvarMap = -- Kernel concessions for Frontend Model and Msg + -- @TODO: Not sure if this should be here or not ("elm", "browser", "Browser.Navigation", "Key") -> ("Browser.Navigation.Key", Set.singleton moduleName, Map.empty) ("elm", "file", "File", "File") -> ("File.File", Set.singleton moduleName, Map.empty) ("elm-explorations", "webgl", "WebGL.Texture", "Texture") -> ("WebGL.Texture.Texture", Set.singleton moduleName, Map.empty) diff --git a/extra/Lamdera/Injection.hs b/extra/Lamdera/Injection.hs index 7084ba640..f06ce7152 100644 --- a/extra/Lamdera/Injection.hs +++ b/extra/Lamdera/Injection.hs @@ -232,6 +232,38 @@ injections outputType mode = function _Lamdera_inject(app) { app.die = app.stop; } + + // In Elm, Browser.Navigation.Key is a function behind the scenes. It is passed and called here. + // In Lamdera, the Key becomes an object after a Wire roundtrip, so we just take the key as a "password" + // but then call the actual function ourselves. We _could_ Wire it as a reference, but then we have a + // new problem: The key function is from the _old_ app and references functions and data from the old app. + // So we would need to be able to update it, both so that it works and to not leak memory from the old app. + // That could be doable by introducing a special reference Wire encoding specifically for Browser.Navigation.Key. + // But even if we do that we still have a problem: Migrating from apps that doesn't have that new Wire encoding yet. + // As long as we want to support migrating such apps, we have to stay with the solution below. + var _Lamdera_navKey = function() {}; + var _Browser_go = F2(function(key, n) { + return A2($$elm$$core$$Task$$perform, $$elm$$core$$Basics$$never, _Scheduler_binding(function() { + n && history.go(n); + _Lamdera_navKey(); + })); + }); + // $$elm$$browser$$Browser$$Navigation$$back is not a direct assignment so it does not need to be replaced. + var $$elm$$browser$$Browser$$Navigation$$forward = _Browser_go; + var _Browser_pushUrl = F2(function(key, url) { + return A2($$elm$$core$$Task$$perform, $$elm$$core$$Basics$$never, _Scheduler_binding(function() { + history.pushState({}, "", url); + _Lamdera_navKey(); + })); + }); + var $$elm$$browser$$Browser$$Navigation$$pushUrl = _Browser_pushUrl; + var _Browser_replaceUrl = F2(function(key, url) { + return A2($$elm$$core$$Task$$perform, $$elm$$core$$Basics$$never, _Scheduler_binding(function() { + history.replaceState({}, "", url); + _Lamdera_navKey(); + })); + }); + var $$elm$$browser$$Browser$$Navigation$$replaceUrl = _Browser_replaceUrl; |] LamderaLive -> diff --git a/extra/Lamdera/Wire3/Decoder.hs b/extra/Lamdera/Wire3/Decoder.hs index 25b564db2..2af46d365 100644 --- a/extra/Lamdera/Wire3/Decoder.hs +++ b/extra/Lamdera/Wire3/Decoder.hs @@ -349,7 +349,8 @@ decoderForType ifaces cname tipe = -- Frontend only JS reference types - TType (Module.Canonical (Name "elm" "browser") "Browser.Navigation") "Key" params -> callDecoder "decodeRef" tipe + -- See Injection.hs for why we don’t decode `Browser.Navigation.Key` as a reference. + -- TType (Module.Canonical (Name "elm" "browser") "Browser.Navigation") "Key" params -> callDecoder "decodeRef" tipe TType (Module.Canonical (Name "elm" "file") "File") "File" params -> callDecoder "decodeRef" tipe TType (Module.Canonical (Name "elm-explorations" "webgl") "WebGL.Texture") "Texture" params -> callDecoder "decodeRef" tipe diff --git a/extra/Lamdera/Wire3/Encoder.hs b/extra/Lamdera/Wire3/Encoder.hs index de54b8c1e..c7bb19203 100644 --- a/extra/Lamdera/Wire3/Encoder.hs +++ b/extra/Lamdera/Wire3/Encoder.hs @@ -212,8 +212,9 @@ encoderForType depth ifaces cname tipe = -- Frontend only JS reference types - TType (Module.Canonical (Name "elm" "browser") "Browser.Navigation") "Key" _ -> - (a (VarForeign mLamdera_Wire "encodeRef" (Forall Map.empty (TLambda tipe tLamdera_Wire_Encoder)))) + -- See Injection.hs for why we don’t encode `Browser.Navigation.Key` as a reference. + -- TType (Module.Canonical (Name "elm" "browser") "Browser.Navigation") "Key" _ -> + -- (a (VarForeign mLamdera_Wire "encodeRef" (Forall Map.empty (TLambda tipe tLamdera_Wire_Encoder)))) TType (Module.Canonical (Name "elm" "file") "File") "File" _ -> (a (VarForeign mLamdera_Wire "encodeRef" (Forall Map.empty (TLambda tipe tLamdera_Wire_Encoder)))) TType (Module.Canonical (Name "elm-explorations" "webgl") "WebGL.Texture") "Texture" _ -> diff --git a/extra/Lamdera/Wire3/Helpers.hs b/extra/Lamdera/Wire3/Helpers.hs index 17b38bd37..a1b822c85 100644 --- a/extra/Lamdera/Wire3/Helpers.hs +++ b/extra/Lamdera/Wire3/Helpers.hs @@ -122,7 +122,8 @@ isUnsupportedKernelType tipe = -- JS types we are supporting through JS ref encodings. These serialisations -- CANNOT BE DECODED OUTSIDE OF THE JS SCOPE THEY WERE ENCODED IN! - TType (Module.Canonical (Name "elm" "browser") "Browser.Navigation") "File" _ -> False + -- See Injection.hs for why we don’t encode `Browser.Navigation.Key` as a reference. + -- TType (Module.Canonical (Name "elm" "browser") "Browser.Navigation") "File" _ -> False TType (Module.Canonical (Name "elm" "file") "File") "File" _ -> False TType (Module.Canonical (Name "elm-explorations" "webgl") "WebGL.Texture") "Texture" _ -> False From a41a5e6d78d1347fd9e2dff15b8f73497fbe4251 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 23 Aug 2025 22:40:43 +0200 Subject: [PATCH 5/6] Make the
 go away in `lamdera live`

---
 compiler/src/Generate/Html.hs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/compiler/src/Generate/Html.hs b/compiler/src/Generate/Html.hs
index b572d0509..875371d15 100644
--- a/compiler/src/Generate/Html.hs
+++ b/compiler/src/Generate/Html.hs
@@ -84,7 +84,7 @@ sandwich_ root moduleName javascript =
 
 
 
-

+