-
Notifications
You must be signed in to change notification settings - Fork 21
Expand file tree
/
Copy pathInjection.hs
More file actions
971 lines (821 loc) · 36.4 KB
/
Copy pathInjection.hs
File metadata and controls
971 lines (821 loc) · 36.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
module Lamdera.Injection where
{- Additional injections into the Elm compiler JS output.
-}
import System.IO.Unsafe (unsafePerformIO)
import qualified System.Exit as Exit
import qualified File
import qualified System.Environment as Env
import qualified System.Directory as Dir
import qualified Data.ByteString.Builder as B
import qualified Data.ByteString as BS
import Language.Haskell.TH (runIO)
import Data.FileEmbed (bsToExp)
import Data.Monoid (mconcat)
import System.FilePath ((</>), takeDirectory)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import qualified Data.Map as Map
import NeatInterpolation
import qualified Data.Name as Name
import qualified Generate.Mode as Mode
import qualified Elm.Package as Pkg
import qualified Elm.ModuleName as ModuleName
import qualified AST.Optimized as Opt
import qualified Elm.Kernel
import qualified Elm.Outline
import qualified Elm.Version
import Lamdera
import qualified Lamdera.Project
import qualified Lamdera.Relative
import StandaloneInstances
import qualified Ext.Common
type Mains = Map.Map ModuleName.Canonical Opt.Main
graphModifications :: Mode.Mode -> Mains -> Map.Map Opt.Global Opt.Node -> Map.Map Opt.Global Opt.Node
graphModifications mode mains graph = do
if mains & mainsInclude ["LocalDev"]
then graph & Map.mapWithKey (modify $ isOptimizedMode mode)
-- & inspect
else graph
modify :: Bool -> Opt.Global -> Opt.Node -> Opt.Node
modify isOptimized v n =
case (v, n) of
(Opt.Global (ModuleName.Canonical (Pkg.Name "elm" "kernel") "Http") name, Opt.Kernel chunks deps) ->
let newChunks =
chunks & fmap (\chunk ->
case chunk of
Elm.Kernel.JS bs | bs & Text.decodeUtf8 & Text.isInfixOf "var _Http_toTask =" ->
bs
& Text.decodeUtf8
& Text.replace "var _Http_toTask =" "var _Http_toTask_REPLACED ="
& (<>) (modifiedHttp_toTask isOptimized)
& Text.encodeUtf8
& Elm.Kernel.JS
_ ->
chunk
)
in
Opt.Kernel newChunks deps
_ ->
n
{- Approach taken from cors-anywhere:
https://github.com/Rob--W/cors-anywhere/blame/master/README.md#L56
Rewrites XHR request URLs from {url} to http://localhost:8001/{url}
Refined to use the shouldProxy global in context of the current msg type
being handled, so we can identify and proxy only BackendMsg task cmds,
leaving Frontend HTTP to still function as normal, including browser CORS
behaviors and limitations.
See extra/Lamdera/ReverseProxy.hs for the proxy itself
-}
modifiedHttp_toTask isOptimized =
onlyIf (not isOptimized)
-- Identical to original except for alterIfProxyRequired addition
[text|
var shouldProxy = false;
var _Http_toTask = F3(function(router, toTask, request)
{
return _Scheduler_binding(function(callback)
{
function done(response) {
callback(toTask(request.expect.a(response)));
}
var xhr = new XMLHttpRequest();
xhr.addEventListener('error', function() { done($$elm$$http$$Http$$NetworkError_); });
xhr.addEventListener('timeout', function() { done($$elm$$http$$Http$$Timeout_); });
xhr.addEventListener('load', function() { done(_Http_toResponse(request.expect.b, xhr)); });
$$elm$$core$$Maybe$$isJust(request.tracker) && _Http_track(router, xhr, request.tracker.a);
try {
request.url = alterIfProxyRequired(request.url)
xhr.open(request.method, request.url, true);
} catch (e) {
return done($$elm$$http$$Http$$BadUrl_(request.url));
}
_Http_configureRequest(xhr, request);
request.body.a && xhr.setRequestHeader('Content-Type', request.body.a);
xhr.send(request.body.b);
return function() { xhr.c = true; xhr.abort(); };
});
});
window.cors_api_host = 'localhost:8001';
var alterIfProxyRequired = function(url) {
if (shouldProxy) {
var cors_api_url = 'http://' + window.cors_api_host + '/';
var origin = window.location.protocol + '//' + window.location.host;
var targetOrigin = /^https?:\/\/([^\/]+)/i.exec(url)
if (targetOrigin && targetOrigin[0].toLowerCase() !== origin &&
targetOrigin[1] !== window.cors_api_host) {
url = cors_api_url + url;
}
return url;
} else {
return url;
}
}
|]
inspect graph =
let pick v n =
case (v, n) of
(Opt.Global (ModuleName.Canonical (Pkg.Name "elm" "kernel") "Http") name, Opt.Kernel _ _) ->
True
_ ->
False
in
debugHaskellPass "graphModifications keys" (graph & Map.filterWithKey pick & Map.toList & take 5) graph
data OutputType = LamderaBackend | LamderaFrontend | LamderaLive | NotLamdera deriving (Eq)
source :: Mode.Mode -> Mains -> B.Builder
source mode mains =
let
outputType =
if mains & mainsInclude ["Backend", "LBR"] then
LamderaBackend
else if mains & mainsInclude ["Frontend", "LFR"] then
LamderaFrontend
else if mains & mainsInclude ["LocalDev"] then
LamderaLive
else
NotLamdera
in
B.byteString $ Text.encodeUtf8 $ injections outputType mode
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 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: {}
} : {};
}
|]
LamderaBackend ->
[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;
}
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) /**/);
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;
function mtime() { // microseconds
if (!isLamderaRuntime) { return 0; }
const hrTime = process.hrtime();
return Math.floor(hrTime[0] * 1000000 + hrTime[1] / 1000);
}
var buriedTimestamp = null;
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;
}
var serializeDuration, logDuration = null;
var start = mtime();
var pair = A2(update, msg, model);
const updateDuration = 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 = {};
}
// 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
} : {};
}
|]
-- LamderaFrontend or LamderaLive
_ ->
let
shouldProxy =
onlyIf (outputType == LamderaLive)
[text|
shouldProxy = $$author$$project$$LocalDev$$shouldProxy(msg)
|]
in
[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 <a> 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 <a> 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 <a> 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;
return ports ? {
ports: ports,
die: die,
bury: bury,
} : {};
}
// 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;
}
// 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);
}
_VirtualDom_addDomNodes(rootDomNode, oldVirtualNode, patches, eventNode);
return (_VirtualDom_lastDomNode = _VirtualDom_applyPatchesHelp(rootDomNode, patches));
}
// 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
-- // but the fix below as suggested causes this problem:
-- // https://github.com/nodejs/node/issues/26115
-- _Bytes_read_string = F3(function (len, bytes, offset) {
-- var decoder = new TextDecoder('utf8', { fatal: true});
-- var sliceView = new DataView(bytes.buffer, bytes.byteOffset + offset, len);
--
-- return _Utils_Tuple2(offset + len, decoder.decode(sliceView));
-- });
--
-- |]
-- var model = null
-- window.addEventListener('bem', function (e) {
-- model = e.detail
-- }, false);
-- window.dispatchEvent(new Event('rbem'));
-- unsafePerformIO $ do
--
-- injectionsM <- Env.lookupEnv "BACKENDINJECTION"
--
-- Lamdera.debug_ $ "Got " <> show injectionsM <> " for BACKENDINJECTION"
--
-- case injectionsM of
-- Just injectionsPath -> do
-- Lamdera.debug_ $ "Injecting " <> injectionsPath <> " into final source"
-- B.byteString <$> File.readUtf8 injectionsPath
--
-- Nothing ->
-- -- No injections, so we'll inject empty string
-- pure ""
{- elm-pkg-js integration
See: https://github.com/supermario/elm-pkg-js
-}
{-# NOINLINE elmPkgJs #-}
elmPkgJs :: Mode.Mode -> B.Builder
elmPkgJs mode =
case mode of
Mode.Dev _ -> do
unsafePerformIO $ do
root <- getProjectRoot "elmPkgJs"
elmPkgJsSources <- safeListDirectory $ root </> "elm-pkg-js"
includesPathM <- Lamdera.Relative.findFile $ root </> "elm-pkg-js-includes.js"
esbuildConfigPathM <- Lamdera.Relative.findFile $ root </> "esbuild.config.js"
esbuildPathM <- Dir.findExecutable "esbuild"
elmJson <- Elm.Outline.read root True
case (esbuildConfigPathM, esbuildPathM, includesPathM) of
(Just esbuildConfigPath, _, _) ->
if Ext.Common.isDebug_
then do
Lamdera.debug_ "🏗️ Building esbuild.config.js"
hasNode <- Dir.findExecutable "node"
minFile <- case hasNode of
Just node -> do
Ext.Common.bash $ "cd " <> takeDirectory esbuildConfigPath <> " && " <> node <> " " <> esbuildConfigPath
Lamdera.Relative.readFile $ root </> "elm-pkg-js-includes.min.js"
Nothing ->
error "Could not find path to node"
case minFile of
Just minFileContents -> do
pure $ Ext.Common.textToBuilder minFileContents
Nothing -> do
error "no min file after compile, run `node esbuild.config.js` to check errors"
else do
Lamdera.debug_ "🏗️🟠 Using dumbJsPackager, ignoring esbuild.config.js in non-dev mode"
dumbJsPackager (useProgramTestOverrides elmJson) root elmPkgJsSources
(_, Just esbuildPath, Just includesPath) ->
if Ext.Common.isDebug_
then do
esbuildIncluder root esbuildPath includesPath
else do
Lamdera.debug_ "🏗️🟠 Using dumbJsPackager, ignoring esbuild in non-dev mode"
dumbJsPackager (useProgramTestOverrides elmJson) root elmPkgJsSources
_ -> do
Lamdera.debug_ "🏗️ Using dumbJsPackager"
dumbJsPackager (useProgramTestOverrides elmJson) root elmPkgJsSources
_ ->
""
useProgramTestOverrides :: Either a Elm.Outline.Outline -> Bool
useProgramTestOverrides elmJson =
case elmJson of
Right (Elm.Outline.App (Elm.Outline.AppOutline _ _ direct _ _ _)) ->
case Map.lookup Lamdera.Project.lamderaProgramTest direct of
Just (Elm.Version.Version major _ _) ->
if major >= 4 then
True
else
False
Nothing ->
False
_ ->
False
esbuildIncluder :: FilePath -> FilePath -> FilePath -> IO B.Builder
esbuildIncluder root esbuildPath includesPath = do
minFile <- Lamdera.Relative.readFile $ root </> "elm-pkg-js-includes.min.js"
case minFile of
Just minFileContents -> do
Lamdera.debug_ "🏗️ Using cached elm-pkg-js-includes.min.js"
pure $ Ext.Common.textToBuilder minFileContents
Nothing -> do
Lamdera.debug_ "🏗️ Building elm-pkg-js-includes.js with esbuild"
-- packaged <- Ext.Common.cq_ esbuildPath [ includesPath, "--bundle", "--global-name=elmPkgJsIncludes" ] ""
(exit, packaged, stdErr) <- Ext.Common.cq_ esbuildPath [ includesPath, "--bundle", "--minify", "--global-name=elmPkgJsIncludes" ] ""
case exit of
Exit.ExitFailure code -> do
atomicPutStrLn stdErr
pure "ESBUILD ERRORS SEE CONSOLE"
Exit.ExitSuccess ->
packaged
& Ext.Common.stringToBuilder
-- & debugHaskell "minified elmpkgjs"
& pure
-- Build individual files? Any point?
-- elmPkgJsSources & mapM
-- (\f ->
-- if ".js" `Text.isSuffixOf` (Text.pack f) || ".ts" `Text.isSuffixOf` (Text.pack f)
-- then do
-- contents <- File.readUtf8 (root </> "elm-pkg-js" </> f)
-- pure $
-- "'" <> Text.encodeUtf8 (Text.pack f) <> "': function(exports){\n" <> contents <> "\nreturn exports;},\n"
-- else
-- pure ""
-- )
lamderaTestRecordingJs :: Text
lamderaTestRecordingJs =
Text.decodeUtf8 $(bsToExp =<< runIO (Lamdera.Relative.readByteString "extra/test-recording.js"))
-- Tries to be clever by injecting `{}` as the `exports` value. Falls over if the target files have been compiled
-- by a packager or if they don't use the `export.init` syntax, i.e. `export async function init() {...}`
dumbJsPackager addTestRecording root elmPkgJsSources = do
wrappedPkgImports <-
mapM
(\f ->
if ".js" `Text.isSuffixOf` (Text.pack f)
then do
contents <- File.readUtf8 (root </> "elm-pkg-js" </> f)
pure $
"'" <> Text.encodeUtf8 (Text.pack f) <> "': function(exports){\n" <> contents <> "\nreturn exports;},\n"
else
pure ""
)
elmPkgJsSources
pure $ B.byteString $ mconcat
[ "const pkgExports = {\n"
<> mconcat wrappedPkgImports
<> (if addTestRecording then
"'program-test-recording.js': function(exports){\n" <> Text.encodeUtf8 lamderaTestRecordingJs <> "\nreturn exports;},\n"
else
""
)
<> "\n}\n"
, "if (typeof window !== 'undefined') {"
, " window.elmPkgJsIncludes = {"
, " init: async function(app) {"
, " for (var pkgId in pkgExports) {"
, " if (pkgExports.hasOwnProperty(pkgId)) {"
, " pkgExports[pkgId]({}).init(app)"
, " }"
, " }"
, " }"
, " }"
, "}"
]
onlyIf :: Bool -> Text -> Text
onlyIf cond t =
if cond
then t
else ""
isOptimizedMode :: Mode.Mode -> Bool
isOptimizedMode mode =
case mode of
Mode.Dev _ -> False
Mode.Prod _ -> True
mainsInclude :: [Name.Name] -> Mains -> Bool
mainsInclude list mains =
case mains & Map.toList of
((ModuleName.Canonical (Pkg.Name author pkg) modul),_):[] ->
if modul `elem` list
then True
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"))