Fix #2 Do not require local figwheel-bridge.js (#5)

* Fix #2 Do not require local figwheel-bridge.js
This commit is contained in:
psdp 2018-06-04 16:10:43 +08:00 committed by Julien Eluard
parent e3fe471c60
commit 8e01bcfc72
5 changed files with 327 additions and 17 deletions

View File

@ -27,8 +27,33 @@ clj -R:repl -m clj-rn.main rebuild-index --help
```
$ clj -R:repl -m clj-rn.main rebuild-index -p android,ios -a genymotion -i real --figwheel-port 3456
```
### Upgrading from an existing project
- If your existing project has a `figwheel-bridge.js` in the root directory, it can be deleted now as `figwheel` task of `clj-rn.main` would create it for you and place it in `target` directory, it is being required by `index.*.js` every time when starting the app with figwheel. Alternatively, you can choose to use your own JS bridge by setting a variable named `:figwheel-bridge` in `clj-rn.conf.edn`, the value should be that of the JS module name. e.g. `./resources/bridge`
- If you are using the default `figwheel-bridge.js` provided by `clj-rn` (originally from `re-natal`), make sure that `cljsbuild` compiler options for `dev` environment look like this below:
```
{:ios
{:source-paths ["src"]
:compiler {:output-to "target/ios/index.js"
:main "env.ios.main"
:output-dir "target/ios"
:optimizations :none
:target :nodejs}}
:android
{:source-paths ["src"]
:compiler {:output-to "target/android/index.js"
:main "env.android.main"
:output-dir "target/android"
:optimizations :none
:target :nodejs}}}
```
Note: it also supports `:preloads` and `:closure-defines` options now thanks to the work done by `re-natal`.
- Supported `figwheel-sidecar` version => `0.5.14`.
## Thanks
Special thanks to @psdp for the initial implementation. Awesome job!

View File

@ -0,0 +1,266 @@
/*
* Originally taken from https://github.com/decker405/figwheel-react-native
*
* @providesModule figwheel-bridge
*/
var debugEnabled = false;
var config = {
basePath: "target/",
googBasePath: 'goog/',
serverPort: 8081
};
var React = require('react');
var createReactClass = require('create-react-class');
var ReactNative = require('react-native');
var WebSocket = require('WebSocket');
var self;
var evaluate = eval; // This is needed, direct calls to eval does not work (RN packager???)
var externalModules = {};
var evalListeners = {};
var asyncImportChain = new Promise(function (succ,fail) {succ(true);});
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']) {
errorStr += "in file " + data['file'] + " ";
}
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) {
return createReactClass({
getInitialState: function () {
return {loaded: false}
},
render: function () {
if (!this.state.loaded) {
var plainStyle = {flex: 1, alignItems: 'center', justifyContent: 'center'};
return (
<ReactNative.View style={plainStyle}>
<ReactNative.Text>Waiting for Figwheel to load files.</ReactNative.Text>
</ReactNative.View>
);
}
return this.state.root;
},
componentDidMount: function () {
var app = this;
if (typeof goog === "undefined") {
loadApp(platform, devHost, function (appRoot) {
app.setState({root: appRoot, loaded: true});
listenToFigwheelMessages();
});
}
}
})
};
function logDebug(msg) {
if (debugEnabled) {
console.log(msg);
}
}
var isChrome = function () {
return typeof importScripts === "function"
};
function asyncImportScripts(url, transform, success, error) {
logDebug('(asyncImportScripts) Importing: ' + url);
asyncImportChain =
asyncImportChain
.then(function (v) {return fetch(url);})
.then(function (response) {
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) {
evaluate(transform(responseText));
fireEvalListenters(url);
success();
return true;
})
.catch(function (e) {
console.error(e);
error();
return true;
});
}
function syncImportScripts(url, success, error) {
try {
importScripts(url);
logDebug('Evaluated: ' + url);
fireEvalListenters(url);
success();
} catch (e) {
console.error(e);
error()
}
}
// Loads js file sync if possible or async.
function importJs(src, success, error) {
var noop = function(){};
var identity = function (arg){return arg};
var successCb = (typeof success == 'function') ? success : noop;
var errorCb = (typeof error == 'function') ? error : noop;
logDebug('(importJs) Importing: ' + src);
if (isChrome()) {
syncImportScripts(src, successCb, errorCb);
} else {
asyncImportScripts(src, identity, successCb, errorCb);
}
}
function interceptRequire() {
var oldRequire = window.require;
console.info("Shimming require");
window.require = function (id) {
console.info("Requiring: " + id);
if (externalModules[id]) {
return externalModules[id];
}
return oldRequire(id);
};
}
function serverBaseUrl(host) {
return "http://" + host + ":" + config.serverPort
}
function isUnDefined(x) {
return typeof x == "undefined";
}
// 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 importIndexJs(fileBasePath) {
var src = fileBasePath + '/index.js';
var transformFn = function(code) {
var defines = code.match(new RegExp ("goog.global.CLOSURE_UNCOMPILED_DEFINES.*?;"));
var deps = code.match(/goog.require\(.*?\);/g);
var transformedCode = defines.concat(deps).join('');
logDebug('transformed index.js: ', transformedCode);
return transformedCode;
};
logDebug('(importIndexJs) Importing: ' + src);
asyncImportScripts(src, transformFn, function(){}, function(){});
}
function loadApp(platform, devHost, onLoadCb) {
var fileBasePath = serverBaseUrl((isChrome() ? "localhost" : devHost)) + "/" + config.basePath + platform;
// callback when app is ready to get the reloadable component
var mainJs = `/env/${platform}/main.js`;
evalListeners.waitForFinalEval = function (url) {
if (url.indexOf(mainJs) > -1) {
assertRootElExists(platform);
onLoadCb(env[platform].main.root_el);
console.info('Done loading Clojure app');
delete evalListeners.waitForFinalEval;
}
};
if (typeof goog === "undefined") {
console.info('Loading Closure base.');
interceptRequire();
// need to know base path here
importJs(fileBasePath + '/goog/base.js', function () {
shimBaseGoog(fileBasePath);
importJs(fileBasePath + '/cljs_deps.js', function () {
importJs(fileBasePath + '/goog/deps.js', function () {
importIndexJs(fileBasePath);
});
});
});
}
}
function startApp(appName, platform, devHost) {
ReactNative.AppRegistry.registerComponent(
appName, () => figwheelApp(platform, devHost));
}
function withModules(moduleById) {
externalModules = moduleById;
return self;
}
function figwheelImportScript(uri, callback) {
importJs(uri.toString(),
function () {callback(true);},
function () {callback(false);})
}
// Goog fixes
function shimBaseGoog(basePath) {
console.info('Shimming goog functions.');
goog.basePath = basePath + '/' + config.googBasePath;
goog.global.FIGWHEEL_WEBSOCKET_CLASS = WebSocket;
goog.global.FIGWHEEL_IMPORT_SCRIPT = figwheelImportScript;
goog.writeScriptSrcNode = importJs;
goog.writeScriptTag_ = function (src, optSourceText) {
importJs(src);
return true;
};
}
self = {
withModules: withModules,
start: startApp
};
module.exports = self;

View File

@ -6,6 +6,12 @@
(def debug-host-rx #"host]\s+\?:\s+@\".*\";")
(defn copy-resource-file! [resource-path target-path]
(let [resource-file (io/resource resource-path)
target-file (io/file target-path)]
(with-open [in (io/input-stream resource-file)]
(io/copy in target-file))))
(defn edit-file-contents! [path replacements-map]
(as-> (slurp path) $
(reduce (fn [contents [match replacement]]
@ -76,7 +82,7 @@
with-out-str))
((partial spit "env/dev/env/config.cljs"))))
(defn rebuild-index-js [platform {:keys [app-name host-ip js-modules resource-dirs]}]
(defn rebuild-index-js [platform {:keys [app-name host-ip js-modules resource-dirs figwheel-bridge]}]
(let [modules (->> (for [dir resource-dirs]
(->> (file-seq (io/file dir))
(filter #(and (not (re-find #"DS_Store" (str %)))
@ -92,12 +98,13 @@
modules-map (zipmap modules modules)
target-file (str "index." (if (= :ios platform) "ios" "android") ".js")]
(try
(-> "var modules={};%s;\nrequire('figwheel-bridge').withModules(modules).start('%s','%s','%s');"
(-> "var modules={};%s;\nrequire('%s').withModules(modules).start('%s','%s','%s');"
(format
(->> modules-map
(map (fn [[module path]]
(str "modules['" module "']=require('" path "')")))
(str/join ";"))
(or figwheel-bridge "./target/figwheel-bridge.js")
app-name
(name platform)
host-ip)
@ -109,3 +116,8 @@
(defn update-ios-rct-web-socket-executor [host]
(edit-file-contents! "node_modules/react-native/Libraries/WebSocket/RCTWebSocketExecutor.m"
{debug-host-rx (str "host] ?: @\"" host "\";")}))
(defn copy-figwheel-bridge []
(io/make-parents "target/.")
(copy-resource-file! "figwheel-bridge.js" "target/figwheel-bridge.js")
(utils/log "Copied figwheel-bridge.js"))

View File

@ -94,24 +94,28 @@
(let [{:keys [build-ids
android-device
ios-device
figwheel-port]} (parse-cli-opts args rebuild-index-task-opts)
hosts-map {:android (core/resolve-dev-host :android android-device)
:ios (core/resolve-dev-host :ios ios-device)}]
figwheel-port]} (parse-cli-opts args rebuild-index-task-opts)
hosts-map {:android (core/resolve-dev-host :android android-device)
:ios (core/resolve-dev-host :ios ios-device)}
{:keys [name
js-modules
resource-dirs
figwheel-bridge]} (get-main-config)]
(when-not figwheel-bridge
(core/copy-figwheel-bridge))
(core/write-env-dev hosts-map figwheel-port)
(doseq [build-id build-ids
:let [host-ip (get hosts-map build-id)
platform-name (if (= build-id :ios) "iOS" "Android")
{:keys [name
js-modules
resource-dirs]} (get-main-config)]]
(core/rebuild-index-js build-id {:app-name name
:host-ip host-ip
:js-modules js-modules
:resource-dirs resource-dirs})
:let [host-ip (get hosts-map build-id)
platform-name (if (= build-id :ios) "iOS" "Android")]]
(core/rebuild-index-js build-id {:app-name name
:host-ip host-ip
:js-modules js-modules
:resource-dirs resource-dirs
:figwheel-bridge figwheel-bridge})
(when (= build-id :ios)
(core/update-ios-rct-web-socket-executor host-ip)
(utils/log "Host in RCTWebSocketExecutor.m was updated"))
(utils/log (format "Dev server host for %s: %s" platform-name host-ip)))))
(utils/log (format "Dev server host for %s: %s" platform-name host-ip)))))
;;; Help

View File

@ -7,9 +7,12 @@
;;; config
(s/def :config/main (s/merge (s/keys :req-un [:config/name]
:opt-un [:config/js-modules :config/resource-dirs])
(s/map-of #{:name :js-modules :resource-dirs} any?)))
:opt-un [:config/js-modules
:config/resource-dirs
:config/figwheel-bridge])
(s/map-of #{:name :js-modules :resource-dirs :figwheel-bridge} any?)))
(s/def :config/name (and not-empty-string? #(re-matches #"^[A-Z][A-Za-z0-9]+$" %)))
(s/def :config/js-modules (s/coll-of not-empty-string?))
(s/def :config/resource-dirs (s/coll-of not-empty-string?))
(s/def :config/figwheel-bridge not-empty-string?)