Skip to content

Instantly share code, notes, and snippets.

@rtfeldman
Created July 20, 2018 23:36
Show Gist options
  • Save rtfeldman/f259128af7fea653876c34cca033ae68 to your computer and use it in GitHub Desktop.
Save rtfeldman/f259128af7fea653876c34cca033ae68 to your computer and use it in GitHub Desktop.
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