-
-
Save rtfeldman/f259128af7fea653876c34cca033ae68 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const websocketPort = 31337; | |
const fs = require("fs"); | |
const chokidar = require('chokidar'); | |
const WebSocketServer = require("ws").Server; | |
const server = new WebSocketServer({ port: websocketPort }); | |
const fileToHack = process.argv[2] || "elm.js"; | |
const varPrefix = process.argv[3] || "_evancz$elm_todomvc$Todo$"; | |
const mainVarName = varPrefix + "main"; | |
const beforeMainInsertionPoint = `var ${mainVarName} =`; | |
const afterMainInsertionPoint = 'var Elm = {};'; | |
const exportsCode = | |
` | |
_elm_lang$virtual_dom$VirtualDom$program = function(impl) { | |
exports = impl; | |
return function() {}; | |
}; | |
_elm_lang$virtual_dom$VirtualDom$programWithFlags = function(impl) { | |
exports = impl; | |
return function() {}; | |
}; | |
_elm_lang$html$Html$programWithFlags = _elm_lang$virtual_dom$VirtualDom$programWithFlags; | |
_elm_lang$html$Html$program = _elm_lang$virtual_dom$VirtualDom$program; | |
`; | |
// TODO make this less brittle. Include some surrounding lines for context, or something. | |
const replayCaseInsertionPoint = "case 'Import'"; | |
function insertBefore(str, insertionPointStr, strToInsert) { | |
const spliceIndex = str.lastIndexOf(insertionPointStr); | |
if (spliceIndex === -1) { | |
console.error("Unable to properly alter " + fileToHack + " - was it compiled with the --debug flag?"); | |
process.exit(1); | |
} | |
return str.slice(0, spliceIndex) + strToInsert + str.slice(spliceIndex); | |
} | |
const beforeMainCode = | |
` | |
var originalProgram = _elm_lang$virtual_dom$VirtualDom$program; | |
var originalProgramWithFlags = _elm_lang$virtual_dom$VirtualDom$programWithFlags; | |
var _$_replaceableView, _$_replaceableUpdate, _$_replaceableInit, _$_replaceableSubs, _$_initialFlags; | |
function hackImpl(impl) { | |
var originalInit = impl.init; | |
var originalView = impl.view; | |
var originalUpdate = impl.update; | |
var originalSubs = impl.subscriptions; | |
// Initialize replacements | |
_$_replaceableInit = function(flags) { | |
// Write down the flags so every time we call the replaced init, we get | |
// the same flags again! | |
_$_initialFlags = flags; | |
return originalInit(flags); | |
}; | |
_$_replaceableView = function(model) { | |
return originalView(model); | |
}; | |
_$_replaceableUpdate = F2(function(msg, model) { | |
return A2(originalUpdate, msg, model); | |
}); | |
_$_replaceableSubs = function(model) { | |
return originalSubs(model); | |
}; | |
// Use the replacements instead | |
impl.view = function(model) { | |
return _$_replaceableView(model); | |
} | |
impl.update = F2(function(msg, model) { | |
return A2(_$_replaceableUpdate, msg, model); | |
}); | |
impl.subscriptions = function(model) { | |
return _$_replaceableSubs(model); | |
} | |
impl.init = function(flags) { | |
return _$_replaceableInit(flags); | |
} | |
return impl; | |
} | |
_elm_lang$virtual_dom$VirtualDom$program = function(impl) { | |
return originalProgram(hackImpl(impl)); | |
}; | |
_elm_lang$virtual_dom$VirtualDom$programWithFlags = function(impl) { | |
return originalProgramWithFlags(hackImpl(impl)); | |
}; | |
_elm_lang$html$Html$programWithFlags = _elm_lang$virtual_dom$VirtualDom$programWithFlags; | |
_elm_lang$html$Html$program = _elm_lang$virtual_dom$VirtualDom$program; | |
var originalInitialize = _elm_lang$core$Native_Platform.initialize; | |
var mainProcess = null; | |
_elm_lang$core$Native_Platform.initialize = function(init, update, subscriptions, renderer) { | |
var originalRawSpawn = _elm_lang$core$Native_Scheduler.rawSpawn; | |
_elm_lang$core$Native_Scheduler.rawSpawn = function(task) { | |
var process = originalRawSpawn(task); | |
if (mainProcess === null) mainProcess = process; | |
return process; | |
} | |
var app = originalInitialize(init, update, subscriptions, renderer) | |
_elm_lang$core$Native_Scheduler.rawSpawn = originalRawSpawn; | |
return app | |
} | |
function sendToUpdate(msg) { | |
if (!mainProcess) throw Error('should have had a process by now'); | |
_elm_lang$core$Native_Scheduler.rawSend(mainProcess, msg) | |
} | |
// Listen for data from the websocket. When we get a message, eval it. | |
var socket = new WebSocket('ws://localhost:${websocketPort}'); | |
socket.onmessage = function(event) { | |
// TODO detect if we're in the process of recompiling. If so, update the | |
// favicon with a spinner, and then have it go back when we've finished. | |
var msg = {ctor: '$_REPLAY_$', _0: event.data}; | |
sendToUpdate(msg); | |
} | |
`; | |
const replayCode = ` | |
case '$_REPLAY_$': | |
var rawHistory = _elm_lang$virtual_dom$VirtualDom_History$encode(model.history); | |
var newProgram = (function(codeToEval) { | |
var exports; | |
eval(codeToEval); | |
return exports; | |
})(msg._0); | |
// Mutate the existing view, update, etc. | |
_$_replaceableView = newProgram.view; | |
_$_replaceableUpdate = newProgram.update; | |
_$_replaceableInit = newProgram.init; | |
_$_replaceableSubs = newProgram.subscriptions; | |
// The rest of this case is an inlined version of VirtualDom.Debug.loadNewHistory | |
// with one modification: instead of using initialUserModel, it calls | |
// init again and discards the commands - because init may have changed! | |
// This also ensures we're replaying the new messages on top of a valid | |
// initialModel, since the structure of Model may have changed as well. | |
function pureUserInit (flags) { | |
return _elm_lang$core$Tuple$first(_$_replaceableInit(flags)); | |
} | |
var pureUserUpdate = F2( | |
function (msg, userModel) { | |
return _elm_lang$core$Tuple$first( | |
A2(_$_replaceableUpdate, msg, userModel)); | |
}); | |
var userModelFromInit = pureUserInit(_$_initialFlags); | |
var decoder = A2(_elm_lang$virtual_dom$VirtualDom_History$decoder, userModelFromInit, pureUserUpdate); | |
var _p2 = A2(_elm_lang$core$Json_Decode$decodeValue, decoder, rawHistory); | |
if (_p2.ctor === 'Err') { | |
return A2( | |
_elm_lang$core$Platform_Cmd_ops['!'], | |
_elm_lang$core$Native_Utils.update( | |
model, | |
{overlay: _elm_lang$virtual_dom$VirtualDom_Overlay$corruptImport}), | |
{ctor: '[]'}); | |
} else { | |
var _p3 = _p2._0._0; | |
return A2( | |
_elm_lang$core$Platform_Cmd_ops['!'], | |
_elm_lang$core$Native_Utils.update( | |
model, | |
{ | |
history: _p2._0._1, | |
state: _elm_lang$virtual_dom$VirtualDom_Debug$Running(_p3), | |
expando: _elm_lang$virtual_dom$VirtualDom_Expando$init(_p3), | |
overlay: _elm_lang$virtual_dom$VirtualDom_Overlay$none | |
}), | |
{ctor: '[]'}); | |
} | |
` | |
// Hack the compiled Elm code | |
let compiledElmJs = fs.readFileSync(fileToHack, {encoding: "utf8"}); | |
// Only insert if it's not already there! Double-inserting breaks things. | |
if (compiledElmJs.indexOf('$_REPLAY_$') === -1) { | |
compiledElmJs = insertBefore(compiledElmJs, replayCaseInsertionPoint, replayCode); | |
compiledElmJs = insertBefore(compiledElmJs, beforeMainInsertionPoint, beforeMainCode); | |
fs.writeFileSync(fileToHack, compiledElmJs, {encoding: "utf8"}); | |
} | |
let listeners = []; | |
server.on("connection", function(ws) { | |
const listener = function(payload) { | |
if (ws.readyState === ws.OPEN) { | |
ws.send(payload); | |
} | |
} | |
listeners.push(listener); | |
}); | |
console.log("Ready!"); | |
chokidar.watch(fileToHack).on('all', (event, path) => { | |
if (event === "change") { | |
let hackedElmJs = fs.readFileSync(fileToHack, {encoding: "utf8"}); | |
hackedElmJs = insertBefore(hackedElmJs, beforeMainInsertionPoint, exportsCode); | |
hackedElmJs = insertBefore(hackedElmJs, afterMainInsertionPoint, "return;"); | |
listeners.forEach(function(listener) { | |
listener(hackedElmJs); | |
}); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment