├── .gitignore ├── LICENSE.md ├── README.md ├── clj-rn.conf.example.edn ├── deps.edn ├── resources └── figwheel-bridge.js └── src └── clj_rn ├── core.clj ├── main.clj ├── shell.clj ├── specs.clj └── utils.clj /.gitignore: -------------------------------------------------------------------------------- 1 | .cpcache/ 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | ### 1. Definitions 5 | 6 | **1.1. “Contributor”** 7 | means each individual or legal entity that creates, contributes to 8 | the creation of, or owns Covered Software. 9 | 10 | **1.2. “Contributor Version”** 11 | means the combination of the Contributions of others (if any) used 12 | by a Contributor and that particular Contributor's Contribution. 13 | 14 | **1.3. “Contribution”** 15 | means Covered Software of a particular Contributor. 16 | 17 | **1.4. “Covered Software”** 18 | means Source Code Form to which the initial Contributor has attached 19 | the notice in Exhibit A, the Executable Form of such Source Code 20 | Form, and Modifications of such Source Code Form, in each case 21 | including portions thereof. 22 | 23 | **1.5. “Incompatible With Secondary Licenses”** 24 | means 25 | 26 | * **(a)** that the initial Contributor has attached the notice described 27 | in Exhibit B to the Covered Software; or 28 | * **(b)** that the Covered Software was made available under the terms of 29 | version 1.1 or earlier of the License, but not also under the 30 | terms of a Secondary License. 31 | 32 | **1.6. “Executable Form”** 33 | means any form of the work other than Source Code Form. 34 | 35 | **1.7. “Larger Work”** 36 | means a work that combines Covered Software with other material, in 37 | a separate file or files, that is not Covered Software. 38 | 39 | **1.8. “License”** 40 | means this document. 41 | 42 | **1.9. “Licensable”** 43 | means having the right to grant, to the maximum extent possible, 44 | whether at the time of the initial grant or subsequently, any and 45 | all of the rights conveyed by this License. 46 | 47 | **1.10. “Modifications”** 48 | means any of the following: 49 | 50 | * **(a)** any file in Source Code Form that results from an addition to, 51 | deletion from, or modification of the contents of Covered 52 | Software; or 53 | * **(b)** any new file in Source Code Form that contains any Covered 54 | Software. 55 | 56 | **1.11. “Patent Claims” of a Contributor** 57 | means any patent claim(s), including without limitation, method, 58 | process, and apparatus claims, in any patent Licensable by such 59 | Contributor that would be infringed, but for the grant of the 60 | License, by the making, using, selling, offering for sale, having 61 | made, import, or transfer of either its Contributions or its 62 | Contributor Version. 63 | 64 | **1.12. “Secondary License”** 65 | means either the GNU General Public License, Version 2.0, the GNU 66 | Lesser General Public License, Version 2.1, the GNU Affero General 67 | Public License, Version 3.0, or any later versions of those 68 | licenses. 69 | 70 | **1.13. “Source Code Form”** 71 | means the form of the work preferred for making modifications. 72 | 73 | **1.14. “You” (or “Your”)** 74 | means an individual or a legal entity exercising rights under this 75 | License. For legal entities, “You” includes any entity that 76 | controls, is controlled by, or is under common control with You. For 77 | purposes of this definition, “control” means **(a)** the power, direct 78 | or indirect, to cause the direction or management of such entity, 79 | whether by contract or otherwise, or **(b)** ownership of more than 80 | fifty percent (50%) of the outstanding shares or beneficial 81 | ownership of such entity. 82 | 83 | 84 | ### 2. License Grants and Conditions 85 | 86 | #### 2.1. Grants 87 | 88 | Each Contributor hereby grants You a world-wide, royalty-free, 89 | non-exclusive license: 90 | 91 | * **(a)** under intellectual property rights (other than patent or trademark) 92 | Licensable by such Contributor to use, reproduce, make available, 93 | modify, display, perform, distribute, and otherwise exploit its 94 | Contributions, either on an unmodified basis, with Modifications, or 95 | as part of a Larger Work; and 96 | * **(b)** under Patent Claims of such Contributor to make, use, sell, offer 97 | for sale, have made, import, and otherwise transfer either its 98 | Contributions or its Contributor Version. 99 | 100 | #### 2.2. Effective Date 101 | 102 | The licenses granted in Section 2.1 with respect to any Contribution 103 | become effective for each Contribution on the date the Contributor first 104 | distributes such Contribution. 105 | 106 | #### 2.3. Limitations on Grant Scope 107 | 108 | The licenses granted in this Section 2 are the only rights granted under 109 | this License. No additional rights or licenses will be implied from the 110 | distribution or licensing of Covered Software under this License. 111 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 112 | Contributor: 113 | 114 | * **(a)** for any code that a Contributor has removed from Covered Software; 115 | or 116 | * **(b)** for infringements caused by: **(i)** Your and any other third party's 117 | modifications of Covered Software, or **(ii)** the combination of its 118 | Contributions with other software (except as part of its Contributor 119 | Version); or 120 | * **(c)** under Patent Claims infringed by Covered Software in the absence of 121 | its Contributions. 122 | 123 | This License does not grant any rights in the trademarks, service marks, 124 | or logos of any Contributor (except as may be necessary to comply with 125 | the notice requirements in Section 3.4). 126 | 127 | #### 2.4. Subsequent Licenses 128 | 129 | No Contributor makes additional grants as a result of Your choice to 130 | distribute the Covered Software under a subsequent version of this 131 | License (see Section 10.2) or under the terms of a Secondary License (if 132 | permitted under the terms of Section 3.3). 133 | 134 | #### 2.5. Representation 135 | 136 | Each Contributor represents that the Contributor believes its 137 | Contributions are its original creation(s) or it has sufficient rights 138 | to grant the rights to its Contributions conveyed by this License. 139 | 140 | #### 2.6. Fair Use 141 | 142 | This License is not intended to limit any rights You have under 143 | applicable copyright doctrines of fair use, fair dealing, or other 144 | equivalents. 145 | 146 | #### 2.7. Conditions 147 | 148 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 149 | in Section 2.1. 150 | 151 | 152 | ### 3. Responsibilities 153 | 154 | #### 3.1. Distribution of Source Form 155 | 156 | All distribution of Covered Software in Source Code Form, including any 157 | Modifications that You create or to which You contribute, must be under 158 | the terms of this License. You must inform recipients that the Source 159 | Code Form of the Covered Software is governed by the terms of this 160 | License, and how they can obtain a copy of this License. You may not 161 | attempt to alter or restrict the recipients' rights in the Source Code 162 | Form. 163 | 164 | #### 3.2. Distribution of Executable Form 165 | 166 | If You distribute Covered Software in Executable Form then: 167 | 168 | * **(a)** such Covered Software must also be made available in Source Code 169 | Form, as described in Section 3.1, and You must inform recipients of 170 | the Executable Form how they can obtain a copy of such Source Code 171 | Form by reasonable means in a timely manner, at a charge no more 172 | than the cost of distribution to the recipient; and 173 | 174 | * **(b)** You may distribute such Executable Form under the terms of this 175 | License, or sublicense it under different terms, provided that the 176 | license for the Executable Form does not attempt to limit or alter 177 | the recipients' rights in the Source Code Form under this License. 178 | 179 | #### 3.3. Distribution of a Larger Work 180 | 181 | You may create and distribute a Larger Work under terms of Your choice, 182 | provided that You also comply with the requirements of this License for 183 | the Covered Software. If the Larger Work is a combination of Covered 184 | Software with a work governed by one or more Secondary Licenses, and the 185 | Covered Software is not Incompatible With Secondary Licenses, this 186 | License permits You to additionally distribute such Covered Software 187 | under the terms of such Secondary License(s), so that the recipient of 188 | the Larger Work may, at their option, further distribute the Covered 189 | Software under the terms of either this License or such Secondary 190 | License(s). 191 | 192 | #### 3.4. Notices 193 | 194 | You may not remove or alter the substance of any license notices 195 | (including copyright notices, patent notices, disclaimers of warranty, 196 | or limitations of liability) contained within the Source Code Form of 197 | the Covered Software, except that You may alter any license notices to 198 | the extent required to remedy known factual inaccuracies. 199 | 200 | #### 3.5. Application of Additional Terms 201 | 202 | You may choose to offer, and to charge a fee for, warranty, support, 203 | indemnity or liability obligations to one or more recipients of Covered 204 | Software. However, You may do so only on Your own behalf, and not on 205 | behalf of any Contributor. You must make it absolutely clear that any 206 | such warranty, support, indemnity, or liability obligation is offered by 207 | You alone, and You hereby agree to indemnify every Contributor for any 208 | liability incurred by such Contributor as a result of warranty, support, 209 | indemnity or liability terms You offer. You may include additional 210 | disclaimers of warranty and limitations of liability specific to any 211 | jurisdiction. 212 | 213 | 214 | ### 4. Inability to Comply Due to Statute or Regulation 215 | 216 | If it is impossible for You to comply with any of the terms of this 217 | License with respect to some or all of the Covered Software due to 218 | statute, judicial order, or regulation then You must: **(a)** comply with 219 | the terms of this License to the maximum extent possible; and **(b)** 220 | describe the limitations and the code they affect. Such description must 221 | be placed in a text file included with all distributions of the Covered 222 | Software under this License. Except to the extent prohibited by statute 223 | or regulation, such description must be sufficiently detailed for a 224 | recipient of ordinary skill to be able to understand it. 225 | 226 | 227 | ### 5. Termination 228 | 229 | **5.1.** The rights granted under this License will terminate automatically 230 | if You fail to comply with any of its terms. However, if You become 231 | compliant, then the rights granted under this License from a particular 232 | Contributor are reinstated **(a)** provisionally, unless and until such 233 | Contributor explicitly and finally terminates Your grants, and **(b)** on an 234 | ongoing basis, if such Contributor fails to notify You of the 235 | non-compliance by some reasonable means prior to 60 days after You have 236 | come back into compliance. Moreover, Your grants from a particular 237 | Contributor are reinstated on an ongoing basis if such Contributor 238 | notifies You of the non-compliance by some reasonable means, this is the 239 | first time You have received notice of non-compliance with this License 240 | from such Contributor, and You become compliant prior to 30 days after 241 | Your receipt of the notice. 242 | 243 | **5.2.** If You initiate litigation against any entity by asserting a patent 244 | infringement claim (excluding declaratory judgment actions, 245 | counter-claims, and cross-claims) alleging that a Contributor Version 246 | directly or indirectly infringes any patent, then the rights granted to 247 | You by any and all Contributors for the Covered Software under Section 248 | 2.1 of this License shall terminate. 249 | 250 | **5.3.** In the event of termination under Sections 5.1 or 5.2 above, all 251 | end user license agreements (excluding distributors and resellers) which 252 | have been validly granted by You or Your distributors under this License 253 | prior to termination shall survive termination. 254 | 255 | 256 | ### 6. Disclaimer of Warranty 257 | 258 | > Covered Software is provided under this License on an “as is” 259 | > basis, without warranty of any kind, either expressed, implied, or 260 | > statutory, including, without limitation, warranties that the 261 | > Covered Software is free of defects, merchantable, fit for a 262 | > particular purpose or non-infringing. The entire risk as to the 263 | > quality and performance of the Covered Software is with You. 264 | > Should any Covered Software prove defective in any respect, You 265 | > (not any Contributor) assume the cost of any necessary servicing, 266 | > repair, or correction. This disclaimer of warranty constitutes an 267 | > essential part of this License. No use of any Covered Software is 268 | > authorized under this License except under this disclaimer. 269 | 270 | ### 7. Limitation of Liability 271 | 272 | > Under no circumstances and under no legal theory, whether tort 273 | > (including negligence), contract, or otherwise, shall any 274 | > Contributor, or anyone who distributes Covered Software as 275 | > permitted above, be liable to You for any direct, indirect, 276 | > special, incidental, or consequential damages of any character 277 | > including, without limitation, damages for lost profits, loss of 278 | > goodwill, work stoppage, computer failure or malfunction, or any 279 | > and all other commercial damages or losses, even if such party 280 | > shall have been informed of the possibility of such damages. This 281 | > limitation of liability shall not apply to liability for death or 282 | > personal injury resulting from such party's negligence to the 283 | > extent applicable law prohibits such limitation. Some 284 | > jurisdictions do not allow the exclusion or limitation of 285 | > incidental or consequential damages, so this exclusion and 286 | > limitation may not apply to You. 287 | 288 | 289 | ### 8. Litigation 290 | 291 | Any litigation relating to this License may be brought only in the 292 | courts of a jurisdiction where the defendant maintains its principal 293 | place of business and such litigation shall be governed by laws of that 294 | jurisdiction, without reference to its conflict-of-law provisions. 295 | Nothing in this Section shall prevent a party's ability to bring 296 | cross-claims or counter-claims. 297 | 298 | 299 | ### 9. Miscellaneous 300 | 301 | This License represents the complete agreement concerning the subject 302 | matter hereof. If any provision of this License is held to be 303 | unenforceable, such provision shall be reformed only to the extent 304 | necessary to make it enforceable. Any law or regulation which provides 305 | that the language of a contract shall be construed against the drafter 306 | shall not be used to construe this License against a Contributor. 307 | 308 | 309 | ### 10. Versions of the License 310 | 311 | #### 10.1. New Versions 312 | 313 | Mozilla Foundation is the license steward. Except as provided in Section 314 | 10.3, no one other than the license steward has the right to modify or 315 | publish new versions of this License. Each version will be given a 316 | distinguishing version number. 317 | 318 | #### 10.2. Effect of New Versions 319 | 320 | You may distribute the Covered Software under the terms of the version 321 | of the License under which You originally received the Covered Software, 322 | or under the terms of any subsequent version published by the license 323 | steward. 324 | 325 | #### 10.3. Modified Versions 326 | 327 | If you create software not governed by this License, and you want to 328 | create a new license for such software, you may create and use a 329 | modified version of this License if you rename the license and remove 330 | any references to the name of the license steward (except to note that 331 | such modified license differs from this License). 332 | 333 | #### 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 334 | 335 | If You choose to distribute Source Code Form that is Incompatible With 336 | Secondary Licenses under the terms of this version of the License, the 337 | notice described in Exhibit B of this License must be attached. 338 | 339 | ## Exhibit A - Source Code Form License Notice 340 | 341 | This Source Code Form is subject to the terms of the Mozilla Public 342 | License, v. 2.0. If a copy of the MPL was not distributed with this 343 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 344 | 345 | If it is not possible or desirable to put the notice in a particular 346 | file, then You may include the notice in a location (such as a LICENSE 347 | file in a relevant directory) where a recipient would be likely to look 348 | for such a notice. 349 | 350 | You may add additional accurate notices of copyright ownership. 351 | 352 | ## Exhibit B - “Incompatible With Secondary Licenses” Notice 353 | 354 | This Source Code Form is "Incompatible With Secondary Licenses", as 355 | defined by the Mozilla Public License, v. 2.0. 356 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CLJ-RN 2 | 3 | A utility for building ClojureScript-based React Native apps 4 | 5 | ## Usage 6 | 7 | This small lib provides ability to start development with just one command and some basic functionality that re-natal has: `enable-source-maps` and `rebuild-index`, which is equivalence of re-natal's `enable-source-maps`, `use-*-device`, `use-figwheel`. 8 | 9 | ``` 10 | clj -m clj-rn.main help 11 | 12 | enable-source-maps Patches RN packager to server *.map files from filesystem, so that chrome can download them. 13 | rebuild-index Generate index.*.js for development with figwheel 14 | watch Start figwheel and cljs repl 15 | help Show this help 16 | ``` 17 | 18 | `watch` has the following options: 19 | ``` 20 | clj -m clj-rn.main watch -h 21 | 22 | -p, --platform BUILD-IDS [:android] Platform Build IDs 23 | -a, --android-device TYPE Android Device Type 24 | -i, --ios-device TYPE iOS Device Type 25 | --[no-]start-app Start `react-native run-*` or not 26 | --[no-]start-figwheel Start Figwheel or not 27 | --[no-]start-cljs-repl Start cljs repl or not 28 | --[no-]start-bundler Start React Native Metro bundler or not 29 | -h, --help 30 | ``` 31 | 32 | ## Setup re-natal project 33 | 34 | 1. Create `deps.edn` file and add `clj-rn` as dependency. More about how to use git libraries here https://clojure.org/guides/deps_and_cli#_using_git_libraries 35 | 2. Create `clj-rn.conf.edn`. See example [clj-rn.conf.example.edn](clj-rn.conf.example.edn) 36 | 3. Run `clj -R:dev -m clj-rn.main watch -p ios -i simulator` to start Figwheel and cljs repl 37 | 38 | **Example usage** 39 | 40 | Run `watch` task which also starts Figwheel and cljs repl: 41 | 42 | ``` 43 | clj -R:dev -m clj-rn.main watch -p android,ios -a genymotion -i real 44 | 45 | ``` 46 | 47 | Start React Native bundler `react-native start` 48 | 49 | Now you can run app `react-native run-android` or `react-native run-ios` 50 | 51 | 52 | ## Possible config options 53 | 54 | - `:name` Name of your project. The same as in `package.json` 55 | - `:builds` Dev builds. You can copy it from `project.clj` 56 | - `:js-modules` List of all used libraries what is required like this `(js/require "...")` for ios and android platforms 57 | - `:desktop-modules` List of all used libraries what is required like this `(js/require "...")` for desktop platform 58 | - `:resource-dirs` Folders with images and other resources 59 | - `:figwheel-bridge.js"` re-natal adds this file when you init project. If you don't have it, you can skip this options 60 | - `:figwheel-options` options for fegwheel server. List of all options https://github.com/bhauman/lein-figwheel/blob/0f62d6d043abb6156393fd167f6c1496c5439689/sidecar/resources/conf-fig-docs/FigwheelOptions.txt 61 | - `:run-options` Those options will be passed to `react-native run-*` 62 | 63 | ## Thanks 64 | 65 | Special thanks to @psdp for the initial implementation. Awesome job! 66 | 67 | ## License 68 | 69 | Licensed under the [Mozilla Public License v2.0](LICENSE.md) 70 | -------------------------------------------------------------------------------- /clj-rn.conf.example.edn: -------------------------------------------------------------------------------- 1 | {:name "CljRnExample" 2 | 3 | ;; dev builds from project.clj 4 | :builds [{:id :ios 5 | :source-paths ["src" "env/dev"] 6 | :figwheel true 7 | :compiler {:output-to "target/ios/app.js" 8 | :main "env.ios.main" 9 | :output-dir "target/ios" 10 | :optimizations :none}} 11 | {:id :android 12 | :source-paths ["src" "env/dev"] 13 | :figwheel true 14 | :compiler {:output-to "target/android/app.js" 15 | :main "env.android.main" 16 | :output-dir "target/android" 17 | :optimizations :none}}] 18 | 19 | :js-modules ["realm"] 20 | :resource-dirs ["images"] 21 | 22 | ;; Don't set it if you don't have this file in your repo 23 | :figwheel-bridge "figwheel-bridge.js" 24 | 25 | ;; All options https://github.com/bhauman/lein-figwheel/blob/0f62d6d043abb6156393fd167f6c1496c5439689/sidecar/resources/conf-fig-docs/FigwheelOptions.txt 26 | :figwheel-options {:nrepl-port 7888 27 | :nrepl-middleware ["cider.nrepl/cider-middleware" 28 | "refactor-nrepl.middleware/wrap-refactor" 29 | "cemerick.piggieback/wrap-cljs-repl"]} 30 | 31 | ;; those options will be passed to `react-native run-*` 32 | :run-options {:ios {} 33 | :android {"appIdPrefix" "debug"} 34 | :default {}}} 35 | 36 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {org.clojure/tools.cli {:mvn/version "0.3.7"} 3 | figwheel-sidecar {:mvn/version "0.5.16" 4 | :exclusions [com.google.javascript/closure-compiler]}}} 5 | -------------------------------------------------------------------------------- /resources/figwheel-bridge.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Originally taken from https://github.com/decker405/figwheel-react-native 3 | * 4 | * @providesModule figwheel-bridge 5 | */ 6 | 7 | var debugEnabled = false; 8 | 9 | var config = { 10 | basePath: "target/", 11 | googBasePath: 'goog/', 12 | serverPort: 8081 13 | }; 14 | 15 | var React = require('react'); 16 | var createReactClass = require('create-react-class'); 17 | var ReactNative = require('react-native'); 18 | var WebSocket = require('WebSocket'); 19 | var self; 20 | var evaluate = eval; // This is needed, direct calls to eval does not work (RN packager???) 21 | var externalModules = {}; 22 | var evalListeners = {}; 23 | var asyncImportChain = new Promise(function (succ,fail) {succ(true);}); 24 | 25 | function fireEvalListenters(url) { 26 | Object.values(evalListeners).forEach(function (listener) { 27 | listener(url) 28 | }); 29 | } 30 | 31 | function formatCompileError(msg) { 32 | var errorStr = "Figwheel Compile Exception: " 33 | var data = msg['exception-data']; 34 | if(data['message']) { 35 | errorStr += data['message'] + " "; 36 | } 37 | if(data['file']) { 38 | errorStr += "in file " + data['file'] + " "; 39 | } 40 | if(data['line']) { 41 | errorStr += "at line " + data['line']; 42 | } 43 | if(data['column']) { 44 | errorStr += ", column " + data['column']; 45 | } 46 | return errorStr; 47 | } 48 | 49 | /* This is simply demonstrating that we can receive and react to 50 | * arbitrary messages from Figwheel this will enable creating a nicer 51 | * feedback system in the Figwheel top level React component. 52 | */ 53 | function figwheelMessageHandler(msg) { 54 | if(msg["msg-name"] == "compile-failed") { 55 | console.warn(formatCompileError(msg)); 56 | } 57 | } 58 | 59 | function listenToFigwheelMessages() { 60 | if(figwheel.client.add_json_message_watch) { 61 | figwheel.client.add_json_message_watch("ReactNativeMessageIntercept", 62 | figwheelMessageHandler); 63 | } 64 | } 65 | 66 | var figwheelApp = function (platform, devHost) { 67 | return createReactClass({ 68 | getInitialState: function () { 69 | return {loaded: false} 70 | }, 71 | render: function () { 72 | if (!this.state.loaded) { 73 | var plainStyle = {flex: 1, alignItems: 'center', justifyContent: 'center'}; 74 | return ( 75 | 76 | Waiting for Figwheel to load files. 77 | 78 | ); 79 | } 80 | return this.state.root; 81 | }, 82 | 83 | componentDidMount: function () { 84 | var app = this; 85 | if (typeof goog === "undefined") { 86 | loadApp(platform, devHost, function (appRoot) { 87 | app.setState({root: appRoot, loaded: true}); 88 | listenToFigwheelMessages(); 89 | }); 90 | } 91 | } 92 | }) 93 | }; 94 | 95 | function logDebug(msg) { 96 | if (debugEnabled) { 97 | console.log(msg); 98 | } 99 | } 100 | 101 | var isChrome = function () { 102 | return typeof importScripts === "function" 103 | }; 104 | 105 | function asyncImportScripts(url, transform, success, error) { 106 | logDebug('(asyncImportScripts) Importing: ' + url); 107 | asyncImportChain = 108 | asyncImportChain 109 | .then(function (v) {return fetch(url);}) 110 | .then(function (response) { 111 | if(response.ok) 112 | return response.text(); 113 | throw new Error("Failed to Fetch: " + url + " - Perhaps your project was cleaned and you haven't recompiled?") 114 | }) 115 | .then(function (responseText) { 116 | evaluate(transform(responseText)); 117 | fireEvalListenters(url); 118 | success(); 119 | return true; 120 | }) 121 | .catch(function (e) { 122 | console.error(e); 123 | error(); 124 | return true; 125 | }); 126 | } 127 | 128 | function syncImportScripts(url, success, error) { 129 | try { 130 | importScripts(url); 131 | logDebug('Evaluated: ' + url); 132 | fireEvalListenters(url); 133 | success(); 134 | } catch (e) { 135 | console.error(e); 136 | error() 137 | } 138 | } 139 | 140 | // Loads js file sync if possible or async. 141 | function importJs(src, success, error) { 142 | var noop = function(){}; 143 | var identity = function (arg){return arg}; 144 | var successCb = (typeof success == 'function') ? success : noop; 145 | var errorCb = (typeof error == 'function') ? error : noop; 146 | logDebug('(importJs) Importing: ' + src); 147 | if (isChrome()) { 148 | syncImportScripts(src, successCb, errorCb); 149 | } else { 150 | asyncImportScripts(src, identity, successCb, errorCb); 151 | } 152 | } 153 | 154 | function interceptRequire() { 155 | var oldRequire = window.require; 156 | console.info("Shimming require"); 157 | window.require = function (id) { 158 | console.info("Requiring: " + id); 159 | if (externalModules[id]) { 160 | return externalModules[id]; 161 | } 162 | return oldRequire(id); 163 | }; 164 | } 165 | 166 | function serverBaseUrl(host) { 167 | return "http://" + host + ":" + config.serverPort 168 | } 169 | 170 | function isUnDefined(x) { 171 | return typeof x == "undefined"; 172 | } 173 | 174 | // unlikely to happen but it happened to me a couple of times so ... 175 | function assertRootElExists(platform) { 176 | var basicMessage = "ClojureScript project didn't compile, or didn't load correctly."; 177 | if(isUnDefined(env)) { 178 | throw new Error("Critical Error: env namespace not defined - " + basicMessage); 179 | } else if(isUnDefined(env[platform])) { 180 | throw new Error("Critical Error: env." + platform + " namespace not defined - " + basicMessage); 181 | } else if(isUnDefined(env[platform].main)) { 182 | throw new Error("Critical Error: env." + platform + ".main namespace not defined - " + basicMessage); 183 | } else if(isUnDefined(env[platform].main.root_el)) { 184 | throw new Error("Critical Error: env." + 185 | platform + ".main namespace doesn't define a root-el which should hold the root react node of your app."); 186 | } 187 | } 188 | 189 | function importIndexJs(fileBasePath) { 190 | var src = fileBasePath + '/index.js'; 191 | var transformFn = function(code) { 192 | var defines = code.match(new RegExp ("goog.global.CLOSURE_UNCOMPILED_DEFINES.*?;")); 193 | var deps = code.match(/goog.require\(.*?\);/g); 194 | var transformedCode = defines.concat(deps).join(''); 195 | logDebug('transformed index.js: ', transformedCode); 196 | return transformedCode; 197 | }; 198 | logDebug('(importIndexJs) Importing: ' + src); 199 | asyncImportScripts(src, transformFn, function(){}, function(){}); 200 | } 201 | 202 | function loadApp(platform, devHost, onLoadCb) { 203 | var fileBasePath = serverBaseUrl((isChrome() ? "localhost" : devHost)) + "/" + config.basePath + platform; 204 | 205 | // callback when app is ready to get the reloadable component 206 | var mainJs = `/env/${platform}/main.js`; 207 | evalListeners.waitForFinalEval = function (url) { 208 | if (url.indexOf(mainJs) > -1) { 209 | assertRootElExists(platform); 210 | onLoadCb(env[platform].main.root_el); 211 | console.info('Done loading Clojure app'); 212 | delete evalListeners.waitForFinalEval; 213 | } 214 | }; 215 | 216 | if (typeof goog === "undefined") { 217 | console.info('Loading Closure base.'); 218 | interceptRequire(); 219 | 220 | // need to know base path here 221 | importJs(fileBasePath + '/goog/base.js', function () { 222 | shimBaseGoog(fileBasePath); 223 | importJs(fileBasePath + '/cljs_deps.js', function () { 224 | importJs(fileBasePath + '/goog/deps.js', function () { 225 | importIndexJs(fileBasePath); 226 | }); 227 | }); 228 | }); 229 | } 230 | } 231 | 232 | function startApp(appName, platform, devHost) { 233 | ReactNative.AppRegistry.registerComponent( 234 | appName, () => figwheelApp(platform, devHost)); 235 | } 236 | 237 | function withModules(moduleById) { 238 | externalModules = moduleById; 239 | return self; 240 | } 241 | 242 | function figwheelImportScript(uri, callback) { 243 | importJs(uri.toString(), 244 | function () {callback(true);}, 245 | function () {callback(false);}) 246 | } 247 | 248 | // Goog fixes 249 | function shimBaseGoog(basePath) { 250 | console.info('Shimming goog functions.'); 251 | goog.basePath = basePath + '/' + config.googBasePath; 252 | goog.global.FIGWHEEL_WEBSOCKET_CLASS = WebSocket; 253 | goog.global.FIGWHEEL_IMPORT_SCRIPT = figwheelImportScript; 254 | goog.writeScriptSrcNode = importJs; 255 | goog.writeScriptTag_ = function (src, optSourceText) { 256 | importJs(src); 257 | return true; 258 | }; 259 | } 260 | 261 | self = { 262 | withModules: withModules, 263 | start: startApp 264 | }; 265 | 266 | module.exports = self; 267 | -------------------------------------------------------------------------------- /src/clj_rn/core.clj: -------------------------------------------------------------------------------- 1 | (ns clj-rn.core 2 | (:require [clj-rn.utils :as utils] 3 | [clojure.java.io :as io] 4 | [clojure.pprint :as pprint] 5 | [clojure.string :as str] 6 | [clj-rn.shell :as shell] 7 | [figwheel-sidecar.repl-api :as ra] 8 | [clojure.java.io :as io] 9 | [clj-rn.utils :as utils] 10 | [clojure.edn :as edn] 11 | [clojure.spec.alpha :as spec] 12 | clj-rn.specs)) 13 | 14 | (def debug-host-rx #"host]\s+\?:\s+@\".*\";") 15 | (def react-native-cli "node_modules/react-native/local-cli/cli.js") 16 | 17 | (defn get-main-config [] 18 | (try 19 | (let [usr-config-file "clj-rn.conf.edn"] 20 | (when-not (.isFile (io/as-file usr-config-file)) 21 | (throw (Exception. "Please create a clj-rn.conf.edn config file"))) 22 | (let [config (edn/read-string (slurp usr-config-file))] 23 | (when-not (spec/valid? :config/main config) 24 | (throw (Exception. "Invalid config."))) 25 | config)) 26 | (catch Exception e 27 | (utils/println-colorized (.getMessage e) shell/color-red) 28 | (System/exit 1)))) 29 | 30 | (defn copy-resource-file! [resource-path target-path] 31 | (let [resource-file (io/resource resource-path) 32 | target-file (io/file target-path)] 33 | (with-open [in (io/input-stream resource-file)] 34 | (io/copy in target-file)))) 35 | 36 | (defn edit-file-contents! [path replacements-map] 37 | (as-> (slurp path) $ 38 | (reduce (fn [contents [match replacement]] 39 | (str/replace contents match replacement)) 40 | $ replacements-map) 41 | (spit path $))) 42 | 43 | (defn enable-source-maps [] 44 | (doseq [path ["node_modules/metro/src/Server/index.js" 45 | "node_modules/metro-bundler/src/Server/index.js" 46 | "node_modules/metro-bundler/build/Server/index.js" 47 | "node_modules/react-native/packager/src/Server/index.js"]] 48 | (when (.exists (io/as-file path)) 49 | (utils/log (str "Patching file " path " to serve *.map files.")) 50 | (spit path 51 | (str/replace (slurp path) "/\\.map$/" "/index\\..*\\.map$/")))) 52 | (utils/log "Source maps enabled.")) 53 | 54 | (defn get-lan-ip 55 | "If .lan-ip file exists, it fetches the ip from the file." 56 | [] 57 | (if-let [ip (try (slurp ".lan-ip") (catch Exception e nil))] 58 | (str/trim-newline ip) 59 | (cond 60 | (some #{(System/getProperty "os.name")} ["Mac OS X" "Windows 10"]) 61 | (.getHostAddress (java.net.InetAddress/getLocalHost)) 62 | :else 63 | (->> (java.net.NetworkInterface/getNetworkInterfaces) 64 | (enumeration-seq) 65 | (filter #(not (or (str/starts-with? (.getName %) "docker") 66 | (str/starts-with? (.getName %) "br-")))) 67 | (map #(.getInterfaceAddresses %)) 68 | (map 69 | (fn [ip] 70 | (seq (filter #(instance? 71 | java.net.Inet4Address 72 | (.getAddress %)) 73 | ip)))) 74 | (remove nil?) 75 | (first) 76 | (filter #(instance? 77 | java.net.Inet4Address 78 | (.getAddress %))) 79 | (first) 80 | (.getAddress) 81 | (.getHostAddress))))) 82 | 83 | (defmulti resolve-dev-host (fn [platform _] platform)) 84 | 85 | (defmethod resolve-dev-host :android [_ device-type] 86 | (case device-type 87 | :real "localhost" 88 | :avd "10.0.2.2" 89 | :genymotion "10.0.3.2" 90 | (get-lan-ip))) 91 | 92 | (defmethod resolve-dev-host :ios [_ device-type] 93 | (if (= device-type :simulator) 94 | "localhost" 95 | (get-lan-ip))) 96 | 97 | (defmethod resolve-dev-host :desktop [_ _] 98 | "localhost") 99 | 100 | (defn write-env-dev [hosts-map & [figwheel-port]] 101 | (-> "(ns env.config)\n\n(def figwheel-urls %s)" 102 | (format (-> (into {} 103 | (for [[platform host] hosts-map] 104 | {platform (str "ws://" host ":" (or figwheel-port 3449) "/figwheel-ws")})) 105 | pprint/pprint 106 | with-out-str)) 107 | ((partial spit "env/dev/env/config.cljs")))) 108 | 109 | (defn get-modules 110 | [resource-dirs js-modules] 111 | (->> (mapcat 112 | (fn [dir] 113 | (let [{:keys [path prefix]} (if (map? dir) 114 | dir 115 | {:path dir 116 | :prefix "./"})] 117 | (->> (file-seq (io/file path)) 118 | (filter #(and (not (re-find #"DS_Store" (str %))) 119 | (.isFile %))) 120 | (map (fn [file] (when-let [unix-path (->> file .toPath .iterator iterator-seq (str/join "/"))] 121 | [(-> (str prefix unix-path) 122 | (str/replace "\\" "/") 123 | (str/replace "@2x" "") 124 | (str/replace "@3x" "")) 125 | (-> (str "./" unix-path) 126 | (str/replace "\\" "/") 127 | (str/replace "@2x" "") 128 | (str/replace "@3x" ""))])))))) 129 | resource-dirs) 130 | (concat js-modules ["react" "react-native" "create-react-class"]) 131 | distinct)) 132 | 133 | (defn generate-modules-map 134 | [modules] 135 | (str/join ";" (map (fn [module] 136 | (let [[module-name path] (if (vector? module) 137 | module 138 | [module module])] 139 | (str "modules['" module-name "']=require('" path "')"))) 140 | modules))) 141 | 142 | (defn rebuild-index-js 143 | [platform {:keys [app-name host-ip js-modules desktop-modules resource-dirs figwheel-bridge]}] 144 | (let [target-file (str "index." (name platform) ".js") 145 | modules (get-modules resource-dirs (if (= platform :desktop) desktop-modules js-modules))] 146 | (try 147 | (spit 148 | target-file 149 | (format 150 | "var modules={};%s;\nrequire('%s').withModules(modules).start('%s','%s','%s');" 151 | (generate-modules-map modules) 152 | (or (str/replace figwheel-bridge #"\.js" "") "./target/figwheel-bridge") 153 | app-name 154 | (name platform) 155 | host-ip)) 156 | (utils/log (str target-file " was regenerated")) 157 | (catch Exception e 158 | (utils/log-err (.getMessage e)))))) 159 | 160 | (defn update-ios-rct-web-socket-executor [host] 161 | (edit-file-contents! "node_modules/react-native/Libraries/WebSocket/RCTWebSocketExecutor.m" 162 | {debug-host-rx (str "host] ?: @\"" host "\";")})) 163 | 164 | (defn copy-figwheel-bridge [] 165 | (io/make-parents "target/.") 166 | (copy-resource-file! "figwheel-bridge.js" "target/figwheel-bridge.js") 167 | (utils/log "Copied figwheel-bridge.js")) 168 | 169 | (defn rebuild-index-files 170 | [build-ids hosts-map] 171 | (let [{:keys [js-modules desktop-modules name resource-dirs figwheel-bridge]} (get-main-config)] 172 | (when-not figwheel-bridge (copy-figwheel-bridge)) 173 | (doseq [build-id build-ids 174 | :let [host-ip (get hosts-map build-id) 175 | platform-name (case build-id 176 | :ios "iOS" 177 | :android "Android" 178 | :desktop "Desktop" 179 | "Unknown")]] 180 | (rebuild-index-js build-id {:app-name name 181 | :host-ip host-ip 182 | :js-modules js-modules 183 | :desktop-modules desktop-modules 184 | :resource-dirs resource-dirs 185 | :figwheel-bridge figwheel-bridge}) 186 | (when (= build-id :ios) 187 | (update-ios-rct-web-socket-executor host-ip) 188 | (utils/println-colorized 189 | "Host in RCTWebSocketExecutor.m was updated" 190 | shell/color-green)) 191 | (utils/println-colorized 192 | (format "Dev server host for %s: %s" platform-name host-ip) 193 | shell/color-green)))) 194 | 195 | (defn- execute-react-native-cli 196 | ([command] (execute-react-native-cli command false)) 197 | ([command async?] 198 | (let [full-command (concat ["node" react-native-cli] command)] 199 | (if async? 200 | (future (shell/exec full-command)) 201 | (shell/exec full-command))))) 202 | 203 | (defn- get-run-options 204 | [config platform] 205 | (reduce 206 | #(conj %1 (str "--" (name (first %2))) (second %2)) 207 | [] 208 | (merge (get-in config [:run-options :default]) (get-in config [:run-options platform])))) 209 | 210 | (defn- run-builds 211 | [build-ids config] 212 | (doseq [build-id build-ids] 213 | (let [run-options (get-run-options config build-id) 214 | command (cons (str "run-" (name build-id)) run-options)] 215 | (execute-react-native-cli command)))) 216 | 217 | (defn- check-react-native-instalation [] 218 | (when-not (.exists (io/file react-native-cli)) 219 | (utils/println-colorized "react-native is not installed locally. If you don't have it in 'package.json' run 'npm install react-native --save'. Note what globally installed react-native won't be used." shell/color-red) 220 | (System/exit 1))) 221 | 222 | (defn watch 223 | [{:keys [build-ids android-device ios-device start-figwheel start-app start-cljs-repl start-bundler]}] 224 | (let [{:keys [figwheel-options builds] :as config} (get-main-config) 225 | hosts-map {:android (resolve-dev-host :android android-device) 226 | :ios (resolve-dev-host :ios ios-device) 227 | :desktop (resolve-dev-host :desktop nil)}] 228 | (check-react-native-instalation) 229 | (enable-source-maps) 230 | (write-env-dev hosts-map) 231 | (rebuild-index-files build-ids hosts-map) 232 | (when start-bundler 233 | (execute-react-native-cli ["start"] true)) 234 | (when start-figwheel 235 | (ra/start-figwheel! 236 | {:build-ids build-ids 237 | :all-builds builds 238 | :figwheel-options figwheel-options})) 239 | (when start-app (run-builds build-ids config)) 240 | (when (and start-figwheel start-cljs-repl) 241 | (ra/cljs-repl) 242 | (when (:nrepl-port figwheel-options) 243 | (spit ".nrepl-port" (:nrepl-port figwheel-options)))))) 244 | -------------------------------------------------------------------------------- /src/clj_rn/main.clj: -------------------------------------------------------------------------------- 1 | (ns clj-rn.main 2 | (:require [clj-rn.core :as core] 3 | [clj-rn.shell :as shell] 4 | [clj-rn.utils :as utils] 5 | [clojure.string :as str] 6 | [clojure.tools.cli :as cli])) 7 | 8 | ;;; Helper functions. 9 | 10 | (def cli-tasks-info 11 | {:enable-source-maps {:desc "Patches RN packager to server *.map files from filesystem, so that chrome can download them."} 12 | :rebuild-index {:desc "Generate index.*.js for development with figwheel"} 13 | :watch {:desc "Start development"} 14 | :help {:desc "Show this help"}}) 15 | 16 | (defn- show-help [] 17 | (doseq [[task {:keys [desc usage]}] cli-tasks-info] 18 | (println (format (str shell/color-yellow "%-20s" shell/color-reset 19 | shell/color-green "%s" shell/color-reset) 20 | (name task) desc)) 21 | (when usage 22 | (println) 23 | (->> usage 24 | (map #(str " " %)) 25 | (str/join "\n") 26 | println) 27 | (println)))) 28 | 29 | (defn print-and-exit [msg] 30 | (println msg) 31 | (System/exit 1)) 32 | 33 | (defn parse-cli-options [args options] 34 | (let [{:keys [options errors summary]} (cli/parse-opts args options)] 35 | (cond 36 | (:help options) (print-and-exit summary) 37 | (not (nil? errors)) (print-and-exit errors) 38 | :else options))) 39 | 40 | (def common-options 41 | [["-p" "--platform BUILD-IDS" "Platform Build IDs " 42 | :id :build-ids 43 | :default [:android] 44 | :parse-fn #(->> (.split % ",") 45 | (map (comp keyword str/lower-case str/trim)) 46 | vec) 47 | :validate [(fn [build-ids] (every? #(some? (#{:android :ios :desktop} %)) build-ids)) "Must be \"android\", \"ios\" and/or \"desktop\""]] 48 | ["-a" "--android-device TYPE" "Android Device Type " 49 | :id :android-device 50 | :parse-fn #(keyword (str/lower-case %)) 51 | :validate [#(some? (#{:avd :genymotion :real} %)) "Must be \"avd\", \"genymotion\", or \"real\""]] 52 | ["-i" "--ios-device TYPE" "iOS Device Type " 53 | :id :ios-device 54 | :parse-fn #(keyword (str/lower-case %)) 55 | :validate [#(some? (#{:simulator :real} %)) "Must be \"simulator\", or \"real\""]]]) 56 | 57 | (defn with-common-options 58 | [options] 59 | (concat common-options options [["-h" "--help"]])) 60 | 61 | ;;; Task dispatching 62 | 63 | (defmulti task (comp keyword str/lower-case #(or % "") first)) 64 | 65 | (defmethod task :default [args] 66 | (println (format "Unknown or missing task. Choose one of: %s\n" 67 | (->> cli-tasks-info 68 | keys 69 | (map name) 70 | (interpose ", ") 71 | (apply str)))) 72 | (show-help) 73 | (System/exit 1)) 74 | 75 | ;;; :enable-source-maps task 76 | 77 | (defmethod task :enable-source-maps [_] 78 | (core/enable-source-maps)) 79 | 80 | ;;; :rebuild-index task 81 | 82 | (def rebuild-index-task-options 83 | (with-common-options 84 | [[nil "--figwheel-port PORT" "Figwheel Port" 85 | :id :figwheel-port 86 | :default 3449 87 | :parse-fn #(Integer/parseInt %) 88 | :validate [#(< 0 % 0x10000) "Must be a number between 0 and 65536"]]])) 89 | 90 | (defmethod task :rebuild-index [[_ & args]] 91 | (let [{:keys [build-ids 92 | android-device 93 | ios-device 94 | figwheel-port]} (parse-cli-options args rebuild-index-task-options) 95 | hosts-map {:android (core/resolve-dev-host :android android-device) 96 | :ios (core/resolve-dev-host :ios ios-device) 97 | :desktop (core/resolve-dev-host :desktop nil)}] 98 | (core/write-env-dev hosts-map figwheel-port) 99 | (core/rebuild-index-files build-ids hosts-map))) 100 | 101 | ;;; :watch task 102 | 103 | (def watch-task-options 104 | (with-common-options 105 | [[nil "--[no-]start-app" "Start `react-native run-*` or not" :default false] 106 | [nil "--[no-]start-figwheel" "Start Figwheel or not" :default true] 107 | [nil "--[no-]start-cljs-repl" "Start cljs repl or not" :default true] 108 | [nil "--[no-]start-bundler" "Start React Native Metro bundler or not" :default false]])) 109 | 110 | (defmethod task :watch [[_ & args]] 111 | (let [options (parse-cli-options args watch-task-options)] 112 | (core/watch options))) 113 | 114 | ;;; :help task 115 | 116 | (defmethod task :help [_] 117 | (show-help) 118 | (System/exit 1)) 119 | 120 | (defn -main [& args] 121 | (task args)) 122 | -------------------------------------------------------------------------------- /src/clj_rn/shell.clj: -------------------------------------------------------------------------------- 1 | (ns clj-rn.shell 2 | (:require [clojure.string :as str] 3 | [clojure.java.io :as io]) 4 | (:import java.lang.Runtime)) 5 | 6 | (def color-reset "\u001b[0m") 7 | (def color-red "\u001b[31m") 8 | (def color-green "\u001b[32m") 9 | (def color-yellow "\u001b[33m") 10 | 11 | (defn exec 12 | [command] 13 | (println command) 14 | (let [process (.exec (Runtime/getRuntime) (into-array command))] 15 | (with-open [reader (io/reader (.getInputStream process))] 16 | (doseq [line (line-seq reader)] 17 | (println line))))) 18 | -------------------------------------------------------------------------------- /src/clj_rn/specs.clj: -------------------------------------------------------------------------------- 1 | (ns clj-rn.specs 2 | (:require [clojure.spec.alpha :as s])) 3 | 4 | (def not-empty-string? 5 | (and string? #(not (empty? %)))) 6 | 7 | ;;; config 8 | 9 | (s/def :config/main (s/merge (s/keys :req-un [:config/name 10 | :config/builds] 11 | :opt-un [:config/js-modules 12 | :config/desktop-modules 13 | :config/resource-dirs 14 | :config/figwheel-bridge 15 | :config/figwheel-options 16 | :config/run-options]) 17 | (s/map-of #{:name 18 | :js-modules 19 | :desktop-modules 20 | :resource-dirs 21 | :figwheel-bridge 22 | :figwheel-options 23 | :builds 24 | :run-options} any?))) 25 | 26 | (s/def :run-options/ios map?) 27 | (s/def :run-options/android map?) 28 | (s/def :run-options/desktop map?) 29 | (s/def :run-options/default map?) 30 | (s/def :config/run-options (s/merge (s/keys :opt-un [:run-options/ios 31 | :run-options/android 32 | :run-options/desktop 33 | :run-options/default]) 34 | (s/map-of #{:ios :android :desktop :default} any?))) 35 | 36 | (s/def :config/name (and not-empty-string? #(re-matches #"^[A-Z][A-Za-z0-9]+$" %))) 37 | (s/def :config/js-modules (s/coll-of not-empty-string?)) 38 | (s/def :config/desktop-modules (s/coll-of not-empty-string?)) 39 | (defn resource-dir? [dir] 40 | (or (not-empty-string? dir) (map? dir))) 41 | (s/def :config/resource-dirs (s/coll-of resource-dir?)) 42 | (s/def :config/figwheel-bridge not-empty-string?) 43 | (s/def :config/figwheel-options map?) 44 | (s/def :config/builds (s/coll-of map?)) 45 | -------------------------------------------------------------------------------- /src/clj_rn/utils.clj: -------------------------------------------------------------------------------- 1 | (ns clj-rn.utils 2 | (:require [clj-rn.shell :as shell])) 3 | 4 | (defn colorizer [c] 5 | (fn [& args] 6 | (str c (apply str args) shell/color-reset))) 7 | 8 | (defn println-colorized [message color] 9 | (println ((colorizer color) message))) 10 | 11 | (defn log 12 | ([s] 13 | (log s shell/color-green)) 14 | ([s color] 15 | (println-colorized s color))) 16 | 17 | (defn log-err [s] 18 | (println-colorized s shell/color-red) 19 | (System/exit 1)) 20 | --------------------------------------------------------------------------------