@curtissimo/elm-hot 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG +40 -0
- package/CODE_OF_CONDUCT.md +74 -0
- package/CONTRIBUTING.md +51 -0
- package/LICENSE.txt +33 -0
- package/README.md +84 -0
- package/package.json +37 -0
- package/resources/hmr.js +519 -0
- package/src/index.js +6 -0
- package/src/inject.js +46 -0
- package/test/client.js +63 -0
- package/test/fixtures/BrowserApplicationCounter.elm +136 -0
- package/test/fixtures/BrowserApplicationCounter.html +22 -0
- package/test/fixtures/BrowserApplicationCounterDeepKey.elm +137 -0
- package/test/fixtures/BrowserApplicationCounterDeepKey.html +18 -0
- package/test/fixtures/BrowserApplicationCounterMultiKey.elm +158 -0
- package/test/fixtures/BrowserApplicationCounterMultiKey.html +22 -0
- package/test/fixtures/BrowserApplicationMissingNavKeyError.elm +62 -0
- package/test/fixtures/BrowserApplicationMissingNavKeyError.html +22 -0
- package/test/fixtures/BrowserApplicationNavKeyMoved.elm +159 -0
- package/test/fixtures/BrowserApplicationNavKeyMoved.html +22 -0
- package/test/fixtures/BrowserApplicationWithNull.elm +142 -0
- package/test/fixtures/BrowserApplicationWithNull.html +22 -0
- package/test/fixtures/BrowserDocumentCounter.elm +57 -0
- package/test/fixtures/BrowserDocumentCounter.html +24 -0
- package/test/fixtures/BrowserElementCounter.elm +55 -0
- package/test/fixtures/BrowserElementCounter.html +24 -0
- package/test/fixtures/BrowserSandboxCounter.elm +48 -0
- package/test/fixtures/BrowserSandboxCounter.html +21 -0
- package/test/fixtures/DebugBrowserApplication.elm +139 -0
- package/test/fixtures/DebugBrowserApplication.html +22 -0
- package/test/fixtures/DebugEmbed.elm +48 -0
- package/test/fixtures/DebugEmbed.html +21 -0
- package/test/fixtures/DebugFullscreen.elm +57 -0
- package/test/fixtures/DebugFullscreen.html +22 -0
- package/test/fixtures/FullScreenEmptyInit.elm +51 -0
- package/test/fixtures/FullScreenEmptyInit.html +19 -0
- package/test/fixtures/InitSideEffects.elm +65 -0
- package/test/fixtures/InitSideEffects.html +27 -0
- package/test/fixtures/MainWithTasks.elm +70 -0
- package/test/fixtures/MainWithTasks.html +21 -0
- package/test/fixtures/MultiMain.html +21 -0
- package/test/fixtures/MultiMain1.elm +48 -0
- package/test/fixtures/MultiMain2.elm +48 -0
- package/test/fixtures/PortsEmbed.elm +64 -0
- package/test/fixtures/PortsEmbed.html +27 -0
- package/test/fixtures/PortsFullscreen.elm +70 -0
- package/test/fixtures/PortsFullscreen.html +26 -0
- package/test/fixtures/build.sh +29 -0
- package/test/fixtures/elm.json +24 -0
- package/test/server.js +73 -0
- package/test/test.js +320 -0
- package/test.sh +16 -0
package/resources/hmr.js
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
//////////////////// HMR BEGIN ////////////////////
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
MIT License http://www.opensource.org/licenses/mit-license.php
|
|
5
|
+
Original Author: Flux Xu @fluxxu
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
A note about the environment that this code runs in...
|
|
10
|
+
|
|
11
|
+
assumed globals:
|
|
12
|
+
- `module` (from Node.js module system and webpack)
|
|
13
|
+
|
|
14
|
+
assumed in scope after injection into the Elm IIFE:
|
|
15
|
+
- `scope` (has an 'Elm' property which contains the public Elm API)
|
|
16
|
+
- various functions defined by Elm which we have to hook such as `_Platform_initialize` and `_Scheduler_binding`
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
if (module.hot) {
|
|
20
|
+
(function () {
|
|
21
|
+
"use strict";
|
|
22
|
+
|
|
23
|
+
//polyfill for IE: https://github.com/fluxxu/elm-hot-loader/issues/16
|
|
24
|
+
if (typeof Object.assign != 'function') {
|
|
25
|
+
Object.assign = function (target) {
|
|
26
|
+
'use strict';
|
|
27
|
+
if (target == null) {
|
|
28
|
+
throw new TypeError('Cannot convert undefined or null to object');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
target = Object(target);
|
|
32
|
+
for (var index = 1; index < arguments.length; index++) {
|
|
33
|
+
var source = arguments[index];
|
|
34
|
+
if (source != null) {
|
|
35
|
+
for (var key in source) {
|
|
36
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
37
|
+
target[key] = source[key];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return target;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Elm 0.19.1 introduced a '$' prefix at the beginning of the symbols it emits,
|
|
47
|
+
// and we check for `Maybe.Just` because we expect it to be present in all Elm programs.
|
|
48
|
+
const elmEnvironment = {
|
|
49
|
+
isApplication: false,
|
|
50
|
+
isDocument: false,
|
|
51
|
+
cmdNone: undefined,
|
|
52
|
+
};
|
|
53
|
+
let elmVersion = "";
|
|
54
|
+
if (typeof elm$core$Maybe$Just !== 'undefined') {
|
|
55
|
+
elmEnvironment.isApplication = typeof elm$browser$Browser$application !== "undefined";
|
|
56
|
+
elmEnvironment.isDocument = typeof elm$browser$Browser$document !== "undefined";
|
|
57
|
+
elmEnvironment.cmdNone = elm$core$Platform$Cmd$none;
|
|
58
|
+
elmVersion = '0.19.0';
|
|
59
|
+
} else if (typeof $elm$core$Maybe$Just !== 'undefined') {
|
|
60
|
+
elmEnvironment.isApplication = typeof $elm$browser$Browser$application !== "undefined";
|
|
61
|
+
elmEnvironment.isDocument = typeof $elm$browser$Browser$document !== "undefined";
|
|
62
|
+
elmEnvironment.cmdNone = $elm$core$Platform$Cmd$none;
|
|
63
|
+
elmVersion = '0.19.1';
|
|
64
|
+
} else
|
|
65
|
+
throw new Error("Could not determine Elm version");
|
|
66
|
+
|
|
67
|
+
var instances = module.hot.data
|
|
68
|
+
? module.hot.data.instances || {}
|
|
69
|
+
: {};
|
|
70
|
+
var uid = module.hot.data
|
|
71
|
+
? module.hot.data.uid || 0
|
|
72
|
+
: 0;
|
|
73
|
+
|
|
74
|
+
if (Object.keys(instances).length === 0) {
|
|
75
|
+
log("[elm-hot] Enabled");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
var cancellers = [];
|
|
79
|
+
|
|
80
|
+
// These 2 variables act as dynamically-scoped variables which are set only when the
|
|
81
|
+
// Elm module's hooked init function is called.
|
|
82
|
+
var initializingInstance = null;
|
|
83
|
+
var swappingInstance = null;
|
|
84
|
+
|
|
85
|
+
module.hot.accept();
|
|
86
|
+
module.hot.dispose(function (data) {
|
|
87
|
+
data.instances = instances;
|
|
88
|
+
data.uid = uid;
|
|
89
|
+
|
|
90
|
+
// Cleanup pending async tasks
|
|
91
|
+
|
|
92
|
+
// First, make sure that no new tasks can be started until we finish replacing the code
|
|
93
|
+
_Scheduler_binding = function () {
|
|
94
|
+
return _Scheduler_fail(new Error('[elm-hot] Inactive Elm instance.'))
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Second, kill pending tasks belonging to the old instance
|
|
98
|
+
if (cancellers.length) {
|
|
99
|
+
log('[elm-hot] Killing ' + cancellers.length + ' running processes...');
|
|
100
|
+
try {
|
|
101
|
+
cancellers.forEach(function (cancel) {
|
|
102
|
+
cancel();
|
|
103
|
+
});
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.warn('[elm-hot] Kill process error: ' + e.message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
function log(message) {
|
|
111
|
+
if (module.hot.verbose) {
|
|
112
|
+
console.log(message)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getId() {
|
|
117
|
+
return ++uid;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function findPublicModules(parent, path) {
|
|
121
|
+
var modules = [];
|
|
122
|
+
for (var key in parent) {
|
|
123
|
+
var child = parent[key];
|
|
124
|
+
var currentPath = path ? path + '.' + key : key;
|
|
125
|
+
if ('init' in child) {
|
|
126
|
+
modules.push({
|
|
127
|
+
path: currentPath,
|
|
128
|
+
module: child
|
|
129
|
+
});
|
|
130
|
+
} else {
|
|
131
|
+
modules = modules.concat(findPublicModules(child, currentPath));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return modules;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function registerInstance(domNode, flags, path, portSubscribes, portSends) {
|
|
138
|
+
var id = getId();
|
|
139
|
+
|
|
140
|
+
var instance = {
|
|
141
|
+
id: id,
|
|
142
|
+
path: path,
|
|
143
|
+
domNode: domNode,
|
|
144
|
+
flags: flags,
|
|
145
|
+
portSubscribes: portSubscribes,
|
|
146
|
+
portSends: portSends,
|
|
147
|
+
lastState: null // last Elm app state (root model)
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return instances[id] = instance
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isFullscreenApp() {
|
|
154
|
+
// Returns true if the Elm app will take over the entire DOM body.
|
|
155
|
+
return elmEnvironment.isApplication || elmEnvironment.isDocument;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function wrapDomNode(node) {
|
|
159
|
+
// When embedding an Elm app into a specific DOM node, Elm will replace the provided
|
|
160
|
+
// DOM node with the Elm app's content. When the Elm app is compiled normally, the
|
|
161
|
+
// original DOM node is reused (its attributes and content changes, but the object
|
|
162
|
+
// in memory remains the same). But when compiled using `--debug`, Elm will completely
|
|
163
|
+
// destroy the original DOM node and instead replace it with 2 brand new nodes: one
|
|
164
|
+
// for your Elm app's content and the other for the Elm debugger UI. In this case,
|
|
165
|
+
// if you held a reference to the DOM node provided for embedding, it would be orphaned
|
|
166
|
+
// after Elm module initialization.
|
|
167
|
+
//
|
|
168
|
+
// So in order to make both cases consistent and isolate us from changes in how Elm
|
|
169
|
+
// does this, we will insert a dummy node to wrap the node for embedding and hold
|
|
170
|
+
// a reference to the dummy node.
|
|
171
|
+
//
|
|
172
|
+
// We will also put a tag on the dummy node so that the Elm developer knows who went
|
|
173
|
+
// behind their back and rudely put stuff in their DOM.
|
|
174
|
+
var dummyNode = document.createElement("div");
|
|
175
|
+
dummyNode.setAttribute("data-elm-hot", "true");
|
|
176
|
+
dummyNode.style.height = "inherit";
|
|
177
|
+
var parentNode = node.parentNode;
|
|
178
|
+
parentNode.replaceChild(dummyNode, node);
|
|
179
|
+
dummyNode.appendChild(node);
|
|
180
|
+
return dummyNode;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function wrapPublicModule(path, module) {
|
|
184
|
+
var originalInit = module.init;
|
|
185
|
+
if (originalInit) {
|
|
186
|
+
module.init = function (args) {
|
|
187
|
+
var elm;
|
|
188
|
+
var portSubscribes = {};
|
|
189
|
+
var portSends = {};
|
|
190
|
+
var domNode = null;
|
|
191
|
+
var flags = null;
|
|
192
|
+
if (typeof args !== 'undefined') {
|
|
193
|
+
// normal case
|
|
194
|
+
domNode = args['node'] && !isFullscreenApp()
|
|
195
|
+
? wrapDomNode(args['node'])
|
|
196
|
+
: document.body;
|
|
197
|
+
flags = args['flags'];
|
|
198
|
+
} else {
|
|
199
|
+
// rare case: Elm allows init to be called without any arguments at all
|
|
200
|
+
domNode = document.body;
|
|
201
|
+
flags = undefined
|
|
202
|
+
}
|
|
203
|
+
initializingInstance = registerInstance(domNode, flags, path, portSubscribes, portSends);
|
|
204
|
+
elm = originalInit(args);
|
|
205
|
+
wrapPorts(elm, portSubscribes, portSends);
|
|
206
|
+
initializingInstance = null;
|
|
207
|
+
return elm;
|
|
208
|
+
};
|
|
209
|
+
} else {
|
|
210
|
+
console.error("Could not find a public module to wrap at path " + path)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function swap(Elm, instance) {
|
|
215
|
+
log('[elm-hot] Hot-swapping module: ' + instance.path);
|
|
216
|
+
|
|
217
|
+
swappingInstance = instance;
|
|
218
|
+
|
|
219
|
+
// remove from the DOM everything that had been created by the old Elm app
|
|
220
|
+
var containerNode = instance.domNode;
|
|
221
|
+
while (containerNode.lastChild) {
|
|
222
|
+
containerNode.removeChild(containerNode.lastChild);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
var m = getAt(instance.path.split('.'), Elm);
|
|
226
|
+
var elm;
|
|
227
|
+
if (m) {
|
|
228
|
+
// prepare to initialize the new Elm module
|
|
229
|
+
var args = { flags: instance.flags };
|
|
230
|
+
if (containerNode === document.body) {
|
|
231
|
+
// fullscreen case: no additional args needed
|
|
232
|
+
} else {
|
|
233
|
+
// embed case: provide a new node for Elm to use
|
|
234
|
+
var nodeForEmbed = document.createElement("div");
|
|
235
|
+
containerNode.appendChild(nodeForEmbed);
|
|
236
|
+
args['node'] = nodeForEmbed;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
elm = m.init(args);
|
|
240
|
+
|
|
241
|
+
Object.keys(instance.portSubscribes).forEach(function (portName) {
|
|
242
|
+
if (portName in elm.ports && 'subscribe' in elm.ports[portName]) {
|
|
243
|
+
var handlers = instance.portSubscribes[portName];
|
|
244
|
+
if (!handlers.length) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
log('[elm-hot] Reconnect ' + handlers.length + ' handler(s) to port \''
|
|
248
|
+
+ portName + '\' (' + instance.path + ').');
|
|
249
|
+
handlers.forEach(function (handler) {
|
|
250
|
+
elm.ports[portName].subscribe(handler);
|
|
251
|
+
});
|
|
252
|
+
} else {
|
|
253
|
+
delete instance.portSubscribes[portName];
|
|
254
|
+
log('[elm-hot] Port was removed: ' + portName);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
Object.keys(instance.portSends).forEach(function (portName) {
|
|
259
|
+
if (portName in elm.ports && 'send' in elm.ports[portName]) {
|
|
260
|
+
log('[elm-hot] Replace old port send with the new send');
|
|
261
|
+
instance.portSends[portName] = elm.ports[portName].send;
|
|
262
|
+
} else {
|
|
263
|
+
delete instance.portSends[portName];
|
|
264
|
+
log('[elm-hot] Port was removed: ' + portName);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
} else {
|
|
268
|
+
log('[elm-hot] Module was removed: ' + instance.path);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
swappingInstance = null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function wrapPorts(elm, portSubscribes, portSends) {
|
|
275
|
+
var portNames = Object.keys(elm.ports || {});
|
|
276
|
+
//hook ports
|
|
277
|
+
if (portNames.length) {
|
|
278
|
+
// hook outgoing ports
|
|
279
|
+
portNames
|
|
280
|
+
.filter(function (name) {
|
|
281
|
+
return 'subscribe' in elm.ports[name];
|
|
282
|
+
})
|
|
283
|
+
.forEach(function (portName) {
|
|
284
|
+
var port = elm.ports[portName];
|
|
285
|
+
var subscribe = port.subscribe;
|
|
286
|
+
var unsubscribe = port.unsubscribe;
|
|
287
|
+
elm.ports[portName] = Object.assign(port, {
|
|
288
|
+
subscribe: function (handler) {
|
|
289
|
+
log('[elm-hot] ports.' + portName + '.subscribe called.');
|
|
290
|
+
if (!portSubscribes[portName]) {
|
|
291
|
+
portSubscribes[portName] = [handler];
|
|
292
|
+
} else {
|
|
293
|
+
//TODO handle subscribing to single handler more than once?
|
|
294
|
+
portSubscribes[portName].push(handler);
|
|
295
|
+
}
|
|
296
|
+
return subscribe.call(port, handler);
|
|
297
|
+
},
|
|
298
|
+
unsubscribe: function (handler) {
|
|
299
|
+
log('[elm-hot] ports.' + portName + '.unsubscribe called.');
|
|
300
|
+
var list = portSubscribes[portName];
|
|
301
|
+
if (list && list.indexOf(handler) !== -1) {
|
|
302
|
+
list.splice(list.lastIndexOf(handler), 1);
|
|
303
|
+
} else {
|
|
304
|
+
console.warn('[elm-hot] ports.' + portName + '.unsubscribe: handler not subscribed');
|
|
305
|
+
}
|
|
306
|
+
return unsubscribe.call(port, handler);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// hook incoming ports
|
|
312
|
+
portNames
|
|
313
|
+
.filter(function (name) {
|
|
314
|
+
return 'send' in elm.ports[name];
|
|
315
|
+
})
|
|
316
|
+
.forEach(function (portName) {
|
|
317
|
+
var port = elm.ports[portName];
|
|
318
|
+
portSends[portName] = port.send;
|
|
319
|
+
elm.ports[portName] = Object.assign(port, {
|
|
320
|
+
send: function (val) {
|
|
321
|
+
return portSends[portName].call(port, val);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return portSubscribes;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/*
|
|
330
|
+
Breadth-first search for a `Browser.Navigation.Key` in the user's app model.
|
|
331
|
+
Returns the key and keypath or null if not found.
|
|
332
|
+
*/
|
|
333
|
+
function findNavKey(rootModel) {
|
|
334
|
+
var queue = [];
|
|
335
|
+
if (isDebuggerModel(rootModel)) {
|
|
336
|
+
/*
|
|
337
|
+
Extract the user's app model from the Elm Debugger's model. The Elm debugger
|
|
338
|
+
can hold multiple references to the user's model (e.g. in its "history"). So
|
|
339
|
+
we must be careful to only search within the "state" part of the Debugger.
|
|
340
|
+
*/
|
|
341
|
+
queue.push({ value: rootModel['state'], keypath: ['state'] });
|
|
342
|
+
} else {
|
|
343
|
+
queue.push({ value: rootModel, keypath: [] });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
while (queue.length !== 0) {
|
|
347
|
+
var item = queue.shift();
|
|
348
|
+
|
|
349
|
+
if (typeof item.value === "undefined" || item.value === null) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// The nav key is identified by a runtime tag added by the elm-hot injector.
|
|
354
|
+
if (item.value.hasOwnProperty("elm-hot-nav-key")) {
|
|
355
|
+
// found it!
|
|
356
|
+
return item;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (typeof item.value !== "object") {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (var propName in item.value) {
|
|
364
|
+
if (!item.value.hasOwnProperty(propName)) continue;
|
|
365
|
+
var newKeypath = item.keypath.slice();
|
|
366
|
+
newKeypath.push(propName);
|
|
367
|
+
queue.push({ value: item.value[propName], keypath: newKeypath })
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
function isDebuggerModel(model) {
|
|
376
|
+
// Up until elm/browser 1.0.2, the Elm debugger could be identified by a
|
|
377
|
+
// property named "expando". But in version 1.0.2 that was renamed to "expandoModel"
|
|
378
|
+
return model
|
|
379
|
+
&& (model.hasOwnProperty("expando") || model.hasOwnProperty("expandoModel"))
|
|
380
|
+
&& model.hasOwnProperty("state");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function getAt(keyPath, obj) {
|
|
384
|
+
return keyPath.reduce(function (xs, x) {
|
|
385
|
+
return (xs && xs[x]) ? xs[x] : null
|
|
386
|
+
}, obj)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function removeNavKeyListeners(navKey) {
|
|
390
|
+
window.removeEventListener('popstate', navKey.value);
|
|
391
|
+
window.navigator.userAgent.indexOf('Trident') < 0 || window.removeEventListener('hashchange', navKey.value);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// hook program creation
|
|
395
|
+
var initialize = _Platform_initialize;
|
|
396
|
+
_Platform_initialize = function (flagDecoder, args, init, update, subscriptions, stepperBuilder) {
|
|
397
|
+
var instance = initializingInstance || swappingInstance;
|
|
398
|
+
var tryFirstRender = !!swappingInstance;
|
|
399
|
+
|
|
400
|
+
var hookedInit = function (args) {
|
|
401
|
+
var initialStateTuple = init(args);
|
|
402
|
+
if (swappingInstance) {
|
|
403
|
+
var oldModel = swappingInstance.lastState;
|
|
404
|
+
var newModel = initialStateTuple.a;
|
|
405
|
+
|
|
406
|
+
if (elmEnvironment.isApplication) {
|
|
407
|
+
var oldKeyLoc = findNavKey(oldModel);
|
|
408
|
+
|
|
409
|
+
// attempt to find the Browser.Navigation.Key in the newly-constructed model
|
|
410
|
+
// and bring it along with the rest of the old data.
|
|
411
|
+
var newKeyLoc = findNavKey(newModel);
|
|
412
|
+
var error = null;
|
|
413
|
+
if (newKeyLoc === null) {
|
|
414
|
+
error = "could not find Browser.Navigation.Key in the new app model";
|
|
415
|
+
} else if (oldKeyLoc === null) {
|
|
416
|
+
error = "could not find Browser.Navigation.Key in the old app model.";
|
|
417
|
+
} else if (newKeyLoc.keypath.toString() !== oldKeyLoc.keypath.toString()) {
|
|
418
|
+
error = "the location of the Browser.Navigation.Key in the model has changed.";
|
|
419
|
+
} else {
|
|
420
|
+
// remove event listeners attached to the old nav key
|
|
421
|
+
removeNavKeyListeners(oldKeyLoc.value);
|
|
422
|
+
|
|
423
|
+
// insert the new nav key into the old model in the exact same location
|
|
424
|
+
var parentKeyPath = oldKeyLoc.keypath.slice(0, -1);
|
|
425
|
+
var lastSegment = oldKeyLoc.keypath.slice(-1)[0];
|
|
426
|
+
var oldParent = getAt(parentKeyPath, oldModel);
|
|
427
|
+
oldParent[lastSegment] = newKeyLoc.value;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (error !== null) {
|
|
431
|
+
console.error("[elm-hot] Hot-swapping " + instance.path + " not possible: " + error);
|
|
432
|
+
oldModel = newModel;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// the heart of the app state hot-swap
|
|
437
|
+
initialStateTuple.a = oldModel;
|
|
438
|
+
|
|
439
|
+
// ignore any Cmds returned by the init during hot-swap
|
|
440
|
+
initialStateTuple.b = elmEnvironment.cmdNone;
|
|
441
|
+
} else {
|
|
442
|
+
// capture the initial state for later
|
|
443
|
+
initializingInstance.lastState = initialStateTuple.a;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return initialStateTuple
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
var hookedStepperBuilder = function (sendToApp, model) {
|
|
450
|
+
var result;
|
|
451
|
+
// first render may fail if shape of model changed too much
|
|
452
|
+
if (tryFirstRender) {
|
|
453
|
+
tryFirstRender = false;
|
|
454
|
+
try {
|
|
455
|
+
result = stepperBuilder(sendToApp, model)
|
|
456
|
+
} catch (e) {
|
|
457
|
+
throw new Error('[elm-hot] Hot-swapping ' + instance.path +
|
|
458
|
+
' is not possible, please reload page. Error: ' + e.message)
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
result = stepperBuilder(sendToApp, model)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return function (nextModel, isSync) {
|
|
465
|
+
if (instance) {
|
|
466
|
+
// capture the state after every step so that later we can restore from it during a hot-swap
|
|
467
|
+
instance.lastState = nextModel
|
|
468
|
+
}
|
|
469
|
+
return result(nextModel, isSync)
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
return initialize(flagDecoder, args, hookedInit, update, subscriptions, hookedStepperBuilder)
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// hook process creation
|
|
477
|
+
var originalBinding = _Scheduler_binding;
|
|
478
|
+
_Scheduler_binding = function (originalCallback) {
|
|
479
|
+
return originalBinding(function () {
|
|
480
|
+
// start the scheduled process, which may return a cancellation function.
|
|
481
|
+
var cancel = originalCallback.apply(this, arguments);
|
|
482
|
+
if (cancel) {
|
|
483
|
+
cancellers.push(cancel);
|
|
484
|
+
return function () {
|
|
485
|
+
cancellers.splice(cancellers.indexOf(cancel), 1);
|
|
486
|
+
return cancel();
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
return cancel;
|
|
490
|
+
});
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
scope['_elm_hot_loader_init'] = function (Elm) {
|
|
494
|
+
// swap instances
|
|
495
|
+
var removedInstances = [];
|
|
496
|
+
for (var id in instances) {
|
|
497
|
+
var instance = instances[id];
|
|
498
|
+
if (instance.domNode.parentNode) {
|
|
499
|
+
swap(Elm, instance);
|
|
500
|
+
} else {
|
|
501
|
+
removedInstances.push(id);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
removedInstances.forEach(function (id) {
|
|
506
|
+
delete instance[id];
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// wrap all public modules
|
|
510
|
+
var publicModules = findPublicModules(Elm);
|
|
511
|
+
publicModules.forEach(function (m) {
|
|
512
|
+
wrapPublicModule(m.path, m.module);
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
})();
|
|
516
|
+
|
|
517
|
+
scope['_elm_hot_loader_init'](scope['Elm']);
|
|
518
|
+
}
|
|
519
|
+
//////////////////// HMR END ////////////////////
|
package/src/index.js
ADDED
package/src/inject.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// inject the HMR code into the Elm compiler's JS output
|
|
5
|
+
function inject(originalElmCodeJS) {
|
|
6
|
+
|
|
7
|
+
const hmrCode = fs.readFileSync(path.join(__dirname, "../resources/hmr.js"), { encoding: "utf8" });
|
|
8
|
+
|
|
9
|
+
// first, verify that we have not been given Elm 0.18 code
|
|
10
|
+
if (originalElmCodeJS.indexOf("_elm_lang$core$Native_Platform.initialize") >= 0) {
|
|
11
|
+
throw new Error("[elm-hot] Elm 0.18 is not supported. Please use fluxxu/elm-hot-loader@0.5.x instead.");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let modifiedCode = originalElmCodeJS;
|
|
15
|
+
|
|
16
|
+
if (originalElmCodeJS.indexOf("elm$browser$Browser$application") !== -1) {
|
|
17
|
+
// attach a tag to Browser.Navigation.Key values. It's not really fair to call this a hack
|
|
18
|
+
// as this entire project is a hack, but this is evil evil evil. We need to be able to find
|
|
19
|
+
// the Browser.Navigation.Key in a user's model so that we do not swap out the new one for
|
|
20
|
+
// the old. But as currently implemented (2018-08-19), there's no good way to detect it.
|
|
21
|
+
// So we will add a property to the key immediately after it's created so that we can find it.
|
|
22
|
+
const navKeyDefinition = /var\s+key\s*=\s*function\s*\(\)\s*{\s*key.a\(\s*onUrlChange\(\s*_Browser_getUrl\(\)\s*\)\s*\);\s*};/;
|
|
23
|
+
function replacementString(match) {
|
|
24
|
+
return match + "\n" + "key['elm-hot-nav-key'] = true;";
|
|
25
|
+
}
|
|
26
|
+
modifiedCode = originalElmCodeJS.replace(navKeyDefinition, replacementString);
|
|
27
|
+
if (modifiedCode === originalElmCodeJS) {
|
|
28
|
+
throw new Error("[elm-hot] Browser.Navigation.Key def not found. Version mismatch?");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// splice in the HMR code
|
|
33
|
+
const regex = /(_Platform_export\([^]*)(}\(this\)\);)/;
|
|
34
|
+
const match = regex.exec(modifiedCode);
|
|
35
|
+
|
|
36
|
+
if (match === null) {
|
|
37
|
+
throw new Error("Compiled JS from the Elm compiler is not valid. You must use the Elm 0.19 compiler.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return modifiedCode.slice(0, match.index)
|
|
41
|
+
+ match[1] + "\n\n" + hmrCode + "\n\n" + match[2];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
inject: inject
|
|
46
|
+
};
|
package/test/client.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/*
|
|
2
|
+
A standalone implementation of Webpack's hot module replacement (HMR) system.
|
|
3
|
+
|
|
4
|
+
THIS CODE RUNS IN THE BROWSER!
|
|
5
|
+
|
|
6
|
+
Provide the minimal implementation of Webpack's HMR API. We don't need any of the
|
|
7
|
+
stuff related to Webpack's ability to bundle disparate types of modules (JS, CSS, etc.).
|
|
8
|
+
|
|
9
|
+
For details on the Webpack HMR API, see https://webpack.js.org/api/hot-module-replacement/
|
|
10
|
+
and the source code of `webpack-hot-middleware`
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// TODO [kl] cleanup the globals
|
|
14
|
+
|
|
15
|
+
var eventSource = null;
|
|
16
|
+
|
|
17
|
+
function connect(programName) {
|
|
18
|
+
// Listen for the server to tell us that an HMR update is available
|
|
19
|
+
eventSource = new EventSource("stream-" + programName);
|
|
20
|
+
eventSource.onmessage = function (evt) {
|
|
21
|
+
var reloadUrl = evt.data;
|
|
22
|
+
var myRequest = new Request(reloadUrl);
|
|
23
|
+
myRequest.cache = "no-cache";
|
|
24
|
+
fetch(myRequest).then(function (response) {
|
|
25
|
+
if (response.ok) {
|
|
26
|
+
response.text().then(function (value) {
|
|
27
|
+
module.hot.apply();
|
|
28
|
+
delete Elm;
|
|
29
|
+
eval(value)
|
|
30
|
+
});
|
|
31
|
+
} else {
|
|
32
|
+
console.error("HMR fetch failed:", response.status, response.statusText);
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Expose the Webpack HMR API
|
|
39
|
+
|
|
40
|
+
var myDisposeCallback = null;
|
|
41
|
+
|
|
42
|
+
// simulate the HMR api exposed by webpack
|
|
43
|
+
var module = {
|
|
44
|
+
hot: {
|
|
45
|
+
accept: function () {
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
dispose: function (callback) {
|
|
49
|
+
myDisposeCallback = callback
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
data: null,
|
|
53
|
+
|
|
54
|
+
apply: function () {
|
|
55
|
+
var newData = {};
|
|
56
|
+
myDisposeCallback(newData);
|
|
57
|
+
module.hot.data = newData
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
verbose: true
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
};
|