Merge pull request #138 from bhauman/master

Create better more durable interactions with Figwheel and more.
This commit is contained in:
Artūr Girenko 2017-09-24 14:48:22 +02:00 committed by GitHub
commit cd8f250372
7 changed files with 127 additions and 150 deletions

4
.gitignore vendored
View File

@ -31,3 +31,7 @@ com_crashlytics_export_strings.xml
# npm # npm
node_modules node_modules
package-lock.json
# Emacs
\.\#*

View File

@ -6,6 +6,9 @@
(enable-console-print!) (enable-console-print!)
(assert (exists? core/init) "Fatal Error - Your core.cljs file doesn't define an 'init' function!!! - Perhaps there was a compilation failure?")
(assert (exists? core/app-root) "Fatal Error - Your core.cljs file doesn't define an 'app-root' function!!! - Perhaps there was a compilation failure?")
(figwheel/watch-and-reload (figwheel/watch-and-reload
:websocket-url "ws://localhost:3449/figwheel-ws" :websocket-url "ws://localhost:3449/figwheel-ws"
:heads-up-display false :heads-up-display false

View File

@ -5,6 +5,9 @@
(enable-console-print!) (enable-console-print!)
(assert (exists? core/init) "Fatal Error - Your core.cljs file doesn't define an 'init' function!!! - Perhaps there was a compilation failure?")
(assert (exists? core/app-root) "Fatal Error - Your core.cljs file doesn't define an 'app-root' function!!! - Perhaps there was a compilation failure?")
(def cnt (r/atom 0)) (def cnt (r/atom 0))
(defn reloader [] @cnt [core/app-root]) (defn reloader [] @cnt [core/app-root])

View File

@ -6,6 +6,9 @@
(enable-console-print!) (enable-console-print!)
(assert (exists? core/init) "Fatal Error - Your core.cljs file doesn't define an 'init' function!!! - Perhaps there was a compilation failure?")
(assert (exists? core/app-root) "Fatal Error - Your core.cljs file doesn't define an 'app-root' function!!! - Perhaps there was a compilation failure?")
(def cnt (r/atom 0)) (def cnt (r/atom 0))
(defn reloader [] @cnt [core/app-root]) (defn reloader [] @cnt [core/app-root])

View File

@ -2,6 +2,9 @@
(:require [$PROJECT_NAME_HYPHENATED$.$PLATFORM$.core :as core] (:require [$PROJECT_NAME_HYPHENATED$.$PLATFORM$.core :as core]
[figwheel.client :as figwheel :include-macros true])) [figwheel.client :as figwheel :include-macros true]))
(assert (exists? core/init) "Fatal Error - Your core.cljs file doesn't define an 'init' function!!! - Perhaps there was a compilation failure?")
(assert (exists? core/app-root) "Fatal Error - Your core.cljs file doesn't define an 'app-root' function!!! - Perhaps there was a compilation failure?")
(enable-console-print!) (enable-console-print!)
(figwheel/watch-and-reload (figwheel/watch-and-reload

View File

@ -18,22 +18,51 @@ var createReactClass = require('create-react-class');
var ReactNative = require('react-native'); var ReactNative = require('react-native');
var WebSocket = require('WebSocket'); var WebSocket = require('WebSocket');
var self; var self;
var scriptQueue = [];
var serverHost = null; // will be set dynamically
var fileBasePath = null; // will be set dynamically
var evaluate = eval; // This is needed, direct calls to eval does not work (RN packager???) var evaluate = eval; // This is needed, direct calls to eval does not work (RN packager???)
var externalModules = {}; var externalModules = {};
var evalListeners = [ // Functions to be called after each js file is loaded and evaluated var evalListeners = {};
function (url) { var asyncImportChain = new Promise(function (succ,fail) {succ(true);});
if (url.indexOf('jsloader') > -1) {
shimJsLoader(); function fireEvalListenters(url) {
Object.values(evalListeners).forEach(function (listener) {
listener(url)
});
}
function formatCompileError(msg) {
var errorStr = "Figwheel Compile Exception: "
var data = msg['exception-data'];
if(data['message']) {
errorStr += data['message'] + " ";
} }
}, if(data['file']) {
function (url) { errorStr += "in file " + data['file'] + " ";
if (url.indexOf('/figwheel/client/socket') > -1) {
setCorrectWebSocketImpl();
} }
}]; if(data['line']) {
errorStr += "at line " + data['line'];
}
if(data['column']) {
errorStr += ", column " + data['column'];
}
return errorStr;
}
/* This is simply demonstrating that we can receive and react to
* arbitrary messages from Figwheel this will enable creating a nicer
* feedback system in the Figwheel top level React component.
*/
function figwheelMessageHandler(msg) {
if(msg["msg-name"] == "compile-failed") {
console.warn(formatCompileError(msg));
}
}
function listenToFigwheelMessages() {
if(figwheel.client.add_json_message_watch) {
figwheel.client.add_json_message_watch("ReactNativeMessageIntercept",
figwheelMessageHandler);
}
}
var figwheelApp = function (platform, devHost) { var figwheelApp = function (platform, devHost) {
return createReactClass({ return createReactClass({
@ -51,11 +80,13 @@ var figwheelApp = function (platform, devHost) {
} }
return this.state.root; return this.state.root;
}, },
componentDidMount: function () { componentDidMount: function () {
var app = this; var app = this;
if (typeof goog === "undefined") { if (typeof goog === "undefined") {
loadApp(platform, devHost, function (appRoot) { loadApp(platform, devHost, function (appRoot) {
app.setState({root: appRoot, loaded: true}) app.setState({root: appRoot, loaded: true});
listenToFigwheelMessages();
}); });
} }
} }
@ -68,50 +99,30 @@ function logDebug(msg) {
} }
} }
// evaluates js code ensuring proper ordering
function customEval(url, javascript, success, error) {
if (scriptQueue.length > 0) {
if (scriptQueue[0] === url) {
try {
evaluate(javascript);
logDebug('Evaluated: ' + url);
scriptQueue.shift();
evalListeners.forEach(function (listener) {
listener(url)
});
success();
} catch (e) {
console.error(e);
error();
}
} else {
setTimeout(function () {
customEval(url, javascript, success, error)
}, 5);
}
} else {
console.error('Something bad happened...');
error()
}
}
var isChrome = function () { var isChrome = function () {
return typeof importScripts === "function" return typeof importScripts === "function"
}; };
function asyncImportScripts(url, success, error) { function asyncImportScripts(url, success, error) {
logDebug('(asyncImportScripts) Importing: ' + url); logDebug('(asyncImportScripts) Importing: ' + url);
scriptQueue.push(url); asyncImportChain =
fetch(url) asyncImportChain
.then(function (v) {return fetch(url);})
.then(function (response) { .then(function (response) {
return response.text() if(response.ok)
return response.text();
throw new Error("Failed to Fetch: " + url + " - Perhaps your project was cleaned and you haven't recompiled?")
}) })
.then(function (responseText) { .then(function (responseText) {
return customEval(url, responseText, success, error); evaluate(responseText);
fireEvalListenters(url);
success();
return true;
}) })
.catch(function (error) { .catch(function (e) {
console.error(error); console.error(e);
return error(); error();
return true;
}); });
} }
@ -119,9 +130,7 @@ function syncImportScripts(url, success, error) {
try { try {
importScripts(url); importScripts(url);
logDebug('Evaluated: ' + url); logDebug('Evaluated: ' + url);
evalListeners.forEach(function (listener) { fireEvalListenters(url);
listener(url)
});
success(); success();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -131,22 +140,14 @@ function syncImportScripts(url, success, error) {
// Loads js file sync if possible or async. // Loads js file sync if possible or async.
function importJs(src, success, error) { function importJs(src, success, error) {
if (typeof success !== 'function') { var noop = function(){};
success = function () { success = (typeof success == 'function') ? success : noop;
}; error = (typeof error == 'function') ? error : noop;
} logDebug('(importJs) Importing: ' + src);
if (typeof error !== 'function') {
error = function () {
};
}
var file = fileBasePath + '/' + src;
logDebug('(importJs) Importing: ' + file);
if (isChrome()) { if (isChrome()) {
syncImportScripts(serverBaseUrl("localhost") + '/' + file, success, error); syncImportScripts(src, success, error);
} else { } else {
asyncImportScripts(serverBaseUrl(serverHost) + '/' + file, success, error); asyncImportScripts(src, success, error);
} }
} }
@ -162,61 +163,51 @@ function interceptRequire() {
}; };
} }
function compileWarningsToYellowBox() {
var log = window.console.log;
var compileWarningRx = /Figwheel: Compile/;
var compileExceptionRx = /Figwheel: Compile Exception/;
var errorInFileRx = /Error on file/;
var isBuffering = false;
var compileExceptionBuffer = "";
window.console.log = function (msg) {
log.apply(window.console, arguments);
if (compileExceptionRx.test(msg)) { // enter buffering mode to get all the messages for exception
isBuffering = true;
compileExceptionBuffer = msg + "\n";
} else if (errorInFileRx.test(msg) && isBuffering) { // exit buffering mode and log buffered messages to YellowBox
isBuffering = false;
console.warn(compileExceptionBuffer + msg);
compileExceptionBuffer = "";
} else if (isBuffering) { //log messages buffering mode
compileExceptionBuffer += msg + "\n";
} else if (compileWarningRx.test(msg)) {
console.warn(msg);
}
};
}
function serverBaseUrl(host) { function serverBaseUrl(host) {
return "http://" + host + ":" + config.serverPort return "http://" + host + ":" + config.serverPort
} }
function setCorrectWebSocketImpl() { function isUnDefined(x) {
figwheel.client.socket.get_websocket_imp = function () { return typeof x == "undefined";
return WebSocket;
};
} }
// unlikely to happen but it happened to me a couple of times so ...
function assertRootElExists(platform) {
var basicMessage = "ClojureScript project didn't compile, or didn't load correctly.";
if(isUnDefined(env)) {
throw new Error("Critical Error: env namespace not defined - " + basicMessage);
} else if(isUnDefined(env[platform])) {
throw new Error("Critical Error: env." + platform + " namespace not defined - " + basicMessage);
} else if(isUnDefined(env[platform].main)) {
throw new Error("Critical Error: env." + platform + ".main namespace not defined - " + basicMessage);
} else if(isUnDefined(env[platform].main.root_el)) {
throw new Error("Critical Error: env." +
platform + ".main namespace doesn't define a root-el which should hold the root react node of your app.");
}
}
function loadApp(platform, devHost, onLoadCb) { function loadApp(platform, devHost, onLoadCb) {
serverHost = devHost; var fileBasePath = serverBaseUrl((isChrome() ? "localhost" : devHost)) + "/" + config.basePath + platform;
fileBasePath = config.basePath + platform;
// callback when app is ready to get the reloadable component // callback when app is ready to get the reloadable component
var mainJs = '/env/' + platform + '/main.js'; var mainJs = '/figwheel/connect/build_' + platform + '.js';
evalListeners.push(function (url) { evalListeners.waitForFinalEval = function (url) {
if (url.indexOf(mainJs) > -1) { if (url.indexOf(mainJs) > -1) {
assertRootElExists(platform);
onLoadCb(env[platform].main.root_el); onLoadCb(env[platform].main.root_el);
console.info('Done loading Clojure app'); console.info('Done loading Clojure app');
delete evalListeners.waitForFinalEval;
} }
}); };
if (typeof goog === "undefined") { if (typeof goog === "undefined") {
console.info('Loading Closure base.'); console.info('Loading Closure base.');
interceptRequire(); interceptRequire();
compileWarningsToYellowBox(); // need to know base path here
importJs('goog/base.js', function () { importJs(fileBasePath + '/goog/base.js', function () {
shimBaseGoog(); shimBaseGoog(fileBasePath);
importJs('cljs_deps.js'); importJs(fileBasePath + '/cljs_deps.js');
importJs('goog/deps.js', function () { importJs(fileBasePath + '/goog/deps.js', function () {
// This is needed because of RN packager // This is needed because of RN packager
// seriously React packager? why. // seriously React packager? why.
var googreq = goog.require; var googreq = goog.require;
@ -237,10 +228,18 @@ function withModules(moduleById) {
return self; return self;
} }
function figwheelImportScript(uri, callback) {
importJs(uri.toString(),
function () {callback(true);},
function () {callback(false);})
}
// Goog fixes // Goog fixes
function shimBaseGoog() { function shimBaseGoog(basePath) {
console.info('Shimming goog functions.'); console.info('Shimming goog functions.');
goog.basePath = 'goog/'; goog.basePath = basePath + '/' + config.googBasePath;
goog.global.FIGWHEEL_WEBSOCKET_CLASS = WebSocket;
goog.global.FIGWHEEL_IMPORT_SCRIPT = figwheelImportScript;
goog.writeScriptSrcNode = importJs; goog.writeScriptSrcNode = importJs;
goog.writeScriptTag_ = function (src, optSourceText) { goog.writeScriptTag_ = function (src, optSourceText) {
importJs(src); importJs(src);
@ -248,44 +247,6 @@ function shimBaseGoog() {
}; };
} }
// Figwheel fixes
// Used by figwheel - uses importScript to load JS rather than <script>'s
function shimJsLoader() {
console.info('==== Shimming jsloader ====');
goog.net.jsloader.load = function (uri, options) {
var deferred = {
callbacks: [],
errbacks: [],
addCallback: function (cb) {
deferred.callbacks.push(cb);
},
addErrback: function (cb) {
deferred.errbacks.push(cb);
},
callAllCallbacks: function () {
while (deferred.callbacks.length > 0) {
deferred.callbacks.shift()();
}
},
callAllErrbacks: function () {
while (deferred.errbacks.length > 0) {
deferred.errbacks.shift()();
}
}
};
// Figwheel needs this to be an async call,
// so that it can add callbacks to deferred
setTimeout(function () {
importJs(uri.getPath(),
deferred.callAllCallbacks,
deferred.callAllErrbacks);
}, 1);
return deferred;
};
}
self = { self = {
withModules: withModules, withModules: withModules,
start: startApp start: startApp

View File

@ -7,7 +7,7 @@
[org.clojure/clojurescript "1.9.542"] [org.clojure/clojurescript "1.9.542"]
$INTERFACE_DEPS$] $INTERFACE_DEPS$]
:plugins [[lein-cljsbuild "1.1.4"] :plugins [[lein-cljsbuild "1.1.4"]
[lein-figwheel "0.5.10"]] [lein-figwheel "0.5.14-SNAPSHOT"]]
:clean-targets ["target/" #_($PLATFORM_CLEAN$)] :clean-targets ["target/" #_($PLATFORM_CLEAN$)]
:aliases {"prod-build" ^{:doc "Recompile code with prod profile."} :aliases {"prod-build" ^{:doc "Recompile code with prod profile."}
["do" "clean" ["do" "clean"
@ -15,7 +15,7 @@
"advanced-build" ^{:doc "Recompile code for production using :advanced compilation."} "advanced-build" ^{:doc "Recompile code for production using :advanced compilation."}
["do" "clean" ["do" "clean"
["with-profile" "advanced" "cljsbuild" "once"]]} ["with-profile" "advanced" "cljsbuild" "once"]]}
:profiles {:dev {:dependencies [[figwheel-sidecar "0.5.10"] :profiles {:dev {:dependencies [[figwheel-sidecar "0.5.14-SNAPSHOT"]
[com.cemerick/piggieback "0.2.1"]] [com.cemerick/piggieback "0.2.1"]]
:source-paths ["src" "env/dev"] :source-paths ["src" "env/dev"]
:cljsbuild {:builds [ :cljsbuild {:builds [