├── .gitignore ├── LICENSE ├── README.md ├── deps.edn ├── pom.xml ├── resources ├── closure_bootstrap.js ├── index.js ├── krell_repl.js ├── main.dev.js └── main.prod.js ├── src ├── deps.cljs └── krell │ ├── api.clj │ ├── assets.clj │ ├── deps.clj │ ├── gen.clj │ ├── main.clj │ ├── net.clj │ ├── passes.clj │ ├── repl.clj │ ├── util.clj │ └── watcher.clj └── test └── krell ├── test_deps.clj └── test_paths.clj /.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml 3 | .idea 4 | # Project exclude paths 5 | /. 6 | *.iml 7 | .krell_repl 8 | .cpcache 9 | target/* 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and documentation 12 | distributed under this Agreement, and 13 | b) in the case of each subsequent Contributor: 14 | i) changes to the Program, and 15 | ii) additions to the Program; 16 | 17 | where such changes and/or additions to the Program originate from and are 18 | distributed by that particular Contributor. A Contribution 'originates' 19 | from a Contributor if it was added to the Program by such Contributor 20 | itself or anyone acting on such Contributor's behalf. Contributions do not 21 | include additions to the Program which: (i) are separate modules of 22 | software distributed in conjunction with the Program under their own 23 | license agreement, and (ii) are not derivative works of the Program. 24 | 25 | "Contributor" means any person or entity that distributes the Program. 26 | 27 | "Licensed Patents" mean patent claims licensable by a Contributor which are 28 | necessarily infringed by the use or sale of its Contribution alone or when 29 | combined with the Program. 30 | 31 | "Program" means the Contributions distributed in accordance with this 32 | Agreement. 33 | 34 | "Recipient" means anyone who receives the Program under this Agreement, 35 | including all Contributors. 36 | 37 | 2. GRANT OF RIGHTS 38 | a) Subject to the terms of this Agreement, each Contributor hereby grants 39 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 40 | reproduce, prepare derivative works of, publicly display, publicly 41 | perform, distribute and sublicense the Contribution of such Contributor, 42 | if any, and such derivative works, in source code and object code form. 43 | b) Subject to the terms of this Agreement, each Contributor hereby grants 44 | Recipient a non-exclusive, worldwide, royalty-free patent license under 45 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 46 | transfer the Contribution of such Contributor, if any, in source code and 47 | object code form. This patent license shall apply to the combination of 48 | the Contribution and the Program if, at the time the Contribution is 49 | added by the Contributor, such addition of the Contribution causes such 50 | combination to be covered by the Licensed Patents. The patent license 51 | shall not apply to any other combinations which include the Contribution. 52 | No hardware per se is licensed hereunder. 53 | c) Recipient understands that although each Contributor grants the licenses 54 | to its Contributions set forth herein, no assurances are provided by any 55 | Contributor that the Program does not infringe the patent or other 56 | intellectual property rights of any other entity. Each Contributor 57 | disclaims any liability to Recipient for claims brought by any other 58 | entity based on infringement of intellectual property rights or 59 | otherwise. As a condition to exercising the rights and licenses granted 60 | hereunder, each Recipient hereby assumes sole responsibility to secure 61 | any other intellectual property rights needed, if any. For example, if a 62 | third party patent license is required to allow Recipient to distribute 63 | the Program, it is Recipient's responsibility to acquire that license 64 | before distributing the Program. 65 | d) Each Contributor represents that to its knowledge it has sufficient 66 | copyright rights in its Contribution, if any, to grant the copyright 67 | license set forth in this Agreement. 68 | 69 | 3. REQUIREMENTS 70 | 71 | A Contributor may choose to distribute the Program in object code form under 72 | its own license agreement, provided that: 73 | 74 | a) it complies with the terms and conditions of this Agreement; and 75 | b) its license agreement: 76 | i) effectively disclaims on behalf of all Contributors all warranties 77 | and conditions, express and implied, including warranties or 78 | conditions of title and non-infringement, and implied warranties or 79 | conditions of merchantability and fitness for a particular purpose; 80 | ii) effectively excludes on behalf of all Contributors all liability for 81 | damages, including direct, indirect, special, incidental and 82 | consequential damages, such as lost profits; 83 | iii) states that any provisions which differ from this Agreement are 84 | offered by that Contributor alone and not by any other party; and 85 | iv) states that source code for the Program is available from such 86 | Contributor, and informs licensees how to obtain it in a reasonable 87 | manner on or through a medium customarily used for software exchange. 88 | 89 | When the Program is made available in source code form: 90 | 91 | a) it must be made available under this Agreement; and 92 | b) a copy of this Agreement must be included with each copy of the Program. 93 | Contributors may not remove or alter any copyright notices contained 94 | within the Program. 95 | 96 | Each Contributor must identify itself as the originator of its Contribution, 97 | if 98 | any, in a manner that reasonably allows subsequent Recipients to identify the 99 | originator of the Contribution. 100 | 101 | 4. COMMERCIAL DISTRIBUTION 102 | 103 | Commercial distributors of software may accept certain responsibilities with 104 | respect to end users, business partners and the like. While this license is 105 | intended to facilitate the commercial use of the Program, the Contributor who 106 | includes the Program in a commercial product offering should do so in a manner 107 | which does not create potential liability for other Contributors. Therefore, 108 | if a Contributor includes the Program in a commercial product offering, such 109 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify 110 | every other Contributor ("Indemnified Contributor") against any losses, 111 | damages and costs (collectively "Losses") arising from claims, lawsuits and 112 | other legal actions brought by a third party against the Indemnified 113 | Contributor to the extent caused by the acts or omissions of such Commercial 114 | Contributor in connection with its distribution of the Program in a commercial 115 | product offering. The obligations in this section do not apply to any claims 116 | or Losses relating to any actual or alleged intellectual property 117 | infringement. In order to qualify, an Indemnified Contributor must: 118 | a) promptly notify the Commercial Contributor in writing of such claim, and 119 | b) allow the Commercial Contributor to control, and cooperate with the 120 | Commercial Contributor in, the defense and any related settlement 121 | negotiations. The Indemnified Contributor may participate in any such claim at 122 | its own expense. 123 | 124 | For example, a Contributor might include the Program in a commercial product 125 | offering, Product X. That Contributor is then a Commercial Contributor. If 126 | that Commercial Contributor then makes performance claims, or offers 127 | warranties related to Product X, those performance claims and warranties are 128 | such Commercial Contributor's responsibility alone. Under this section, the 129 | Commercial Contributor would have to defend claims against the other 130 | Contributors related to those performance claims and warranties, and if a 131 | court requires any other Contributor to pay any damages as a result, the 132 | Commercial Contributor must pay those damages. 133 | 134 | 5. NO WARRANTY 135 | 136 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN 137 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 138 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each 140 | Recipient is solely responsible for determining the appropriateness of using 141 | and distributing the Program and assumes all risks associated with its 142 | exercise of rights under this Agreement , including but not limited to the 143 | risks and costs of program errors, compliance with applicable laws, damage to 144 | or loss of data, programs or equipment, and unavailability or interruption of 145 | operations. 146 | 147 | 6. DISCLAIMER OF LIABILITY 148 | 149 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 150 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 151 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 152 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 153 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 154 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 155 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 156 | OF SUCH DAMAGES. 157 | 158 | 7. GENERAL 159 | 160 | If any provision of this Agreement is invalid or unenforceable under 161 | applicable law, it shall not affect the validity or enforceability of the 162 | remainder of the terms of this Agreement, and without further action by the 163 | parties hereto, such provision shall be reformed to the minimum extent 164 | necessary to make such provision valid and enforceable. 165 | 166 | If Recipient institutes patent litigation against any entity (including a 167 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 168 | (excluding combinations of the Program with other software or hardware) 169 | infringes such Recipient's patent(s), then such Recipient's rights granted 170 | under Section 2(b) shall terminate as of the date such litigation is filed. 171 | 172 | All Recipient's rights under this Agreement shall terminate if it fails to 173 | comply with any of the material terms or conditions of this Agreement and does 174 | not cure such failure in a reasonable period of time after becoming aware of 175 | such noncompliance. If all Recipient's rights under this Agreement terminate, 176 | Recipient agrees to cease use and distribution of the Program as soon as 177 | reasonably practicable. However, Recipient's obligations under this Agreement 178 | and any licenses granted by Recipient relating to the Program shall continue 179 | and survive. 180 | 181 | Everyone is permitted to copy and distribute copies of this Agreement, but in 182 | order to avoid inconsistency the Agreement is copyrighted and may only be 183 | modified in the following manner. The Agreement Steward reserves the right to 184 | publish new versions (including revisions) of this Agreement from time to 185 | time. No one other than the Agreement Steward has the right to modify this 186 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 187 | Eclipse Foundation may assign the responsibility to serve as the Agreement 188 | Steward to a suitable separate entity. Each new version of the Agreement will 189 | be given a distinguishing version number. The Program (including 190 | Contributions) may always be distributed subject to the version of the 191 | Agreement under which it was received. In addition, after a new version of the 192 | Agreement is published, Contributor may elect to distribute the Program 193 | (including its Contributions) under the new version. Except as expressly 194 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 195 | licenses to the intellectual property of any Contributor under this Agreement, 196 | whether expressly, by implication, estoppel or otherwise. All rights in the 197 | Program not expressly granted under this Agreement are reserved. 198 | 199 | This Agreement is governed by the laws of the State of New York and the 200 | intellectual property laws of the United States of America. No party to this 201 | Agreement will bring a legal action under this Agreement more than one year 202 | after the cause of action arose. Each party waives its rights to a jury trial in 203 | any resulting litigation. 204 | 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # krell 2 | 3 | Stand alone, low configuration [ClojureScript](https://clojurescript.org) 4 | tooling for [React Native](https://reactnative.dev). All features are provided 5 | as a simple set of defaults over the standard ClojureScript compiler. 6 | 7 | ## Releases & Dependency Information 8 | 9 | ``` 10 | io.vouch/krell {:mvn/version "0.5.4"} 11 | ``` 12 | 13 | ## Why Krell? 14 | 15 | There are two other relatively mature ClojureScript tools for React Native, 16 | [re-natal](https://github.com/drapanjanas/re-natal) and 17 | [shadow-cljs](https://github.com/thheller/shadow-cljs). re-natal is oldest and 18 | likely most widely used. Unfortunately, re-natal is Leiningen-centric and has 19 | some historical design decisions that lead to a stateful CLI API. Still, it is a 20 | direct inspiration for Krell, and re-natal solved many tricky edge-cases early 21 | on. shadow-cljs also offers react-native integration, but provides that as part 22 | of a full featured package rather than an à la carte tool. 23 | 24 | Krell fills the gap by providing a standalone tool with few dependencies. It 25 | does only one thing - extend the standard ClojureScript compiler to make 26 | developing React Native simpler (and easier). 27 | 28 | It does not attempt to paper over the React Native CLI workflow at all. Krell 29 | just provides minimal sensible defaults for development and production and 30 | allows you to switch between these defaults via the familiar ClojureScript 31 | `:optimizations` settings. 32 | 33 | With little configuration beyond typical ClojureScript web config, you get a 34 | React Native REPL-based workflow. Because Krell uses the ClojureScript compiler 35 | to index `node_modules`, you can idiomatically require anything you've installed 36 | via `yarn` or `npm` just like any ClojureScript library. 37 | 38 | If you specify a higher optimization setting like `:simple` or `:advanced`, 39 | Krell generates a single file output without the REPL dependencies. 40 | 41 | ## Requirements 42 | 43 | * Node >= 10.16.0 44 | * Latest release of ClojureScript 45 | * Java SDK 8+ 46 | 47 | Using React Native >= 0.60 is highly recommended as autolinking simplifies usage 48 | greatly. If you must use an older version of React Native refer to the 49 | documentation for the REPL support dependencies: 50 | [react-native-tcp-socket](https://www.npmjs.com/package/react-native-tcp-socket). 51 | 52 | ## Install REPL Dependencies 53 | 54 | Install the REPL support dependencies: 55 | 56 | ``` 57 | clj -M -m cljs.main --install-deps 58 | ``` 59 | 60 | If this fails try: 61 | 62 | ``` 63 | clj -M -m cljs.main -co "{:deps-cmd \"yarn\"}" --install-deps 64 | ``` 65 | 66 | Switch into the `ios` directory of your project and run `pod install`. 67 | 68 | ## REPL 69 | 70 | First build your project: 71 | 72 | ``` 73 | clj -M -m krell.main -v -co build.edn -c 74 | ``` 75 | 76 | Run your React Native project and verify that it works. 77 | 78 | You can start a REPL and connect to the running app at any time: 79 | 80 | ``` 81 | clj -M -m krell.main -co build.edn -r 82 | ``` 83 | 84 | You can of course combine these steps just as with plain `cljs.main`. 85 | 86 | ``` 87 | clj -M -m krell.main -co build.edn -c -r 88 | ``` 89 | 90 | ## Assets & Arbitrary Node Library Requires 91 | 92 | Krell supports arbitrary `js/require` of assets and Node.js dependencies. The 93 | asset support is intended to align with React Native's own documentation - you 94 | must use static relative paths. The additional support for Node.js dependencies 95 | is useful when transitioning away from re-natal to Krell. 96 | 97 | It's important to note that adding a new asset or new Node library currently 98 | requires recompiling the project. 99 | 100 | Other than that there are no other limitations. The handling of `js/require` is 101 | implemented as an analyzer pass so if you want to create macros to generate 102 | asset requires, that will work. 103 | 104 | ## Extending Krell 105 | 106 | Integrating with tools like [Expo](https://expo.dev), and [Storybook.js](https://storybook.js.org) 107 | require providing a custom `index.js` file. This can be provided by one of two 108 | ways - either via the `--index-js` command line flag or by providing a file 109 | called `krell_index.js` on the classpath. The later method opens the door to 110 | extending Krell to other targets without requiring any configuration or wrapping 111 | of Krell at all. 112 | 113 | ## Multiple App Instance Development 114 | 115 | Sometimes it's necessary run multiple instances of the same application during 116 | development - i.e. a chat/messaging application. When Krell starts up it logs 117 | the device ID, for example: 118 | 119 | ``` 120 | Krell sez howdy, Device ID: iPhone12,3 121 | ``` 122 | 123 | You can use this device ID to assign a specific Krell port in your `app.json`, 124 | for example: 125 | 126 | ``` 127 | { 128 | "name": "MyAwesomeProject", 129 | "displayName": "My Awesome Project", 130 | "krellPortMap": { 131 | "iPhone12,3": 5002 132 | } 133 | } 134 | ``` 135 | 136 | Then you can connect to it like so: 137 | 138 | ``` 139 | clj -M -m krell.main -co build.edn -p 5002 -r 140 | ``` 141 | 142 | *You should only have **one** hotloading REPL*. You can disable hot reloading 143 | like so: 144 | 145 | ``` 146 | clj -M -m krell.main -co build.edn -p 5002 -rc false -r 147 | ``` 148 | 149 | ## Examples 150 | 151 | See the [Reagent example tutorial](https://github.com/vouchio/krell/wiki/Reagent-Tutorial) 152 | in the wiki. 153 | 154 | ## Tooling Integration 155 | 156 | [See the wiki](https://github.com/vouch-opensource/krell/wiki/Tooling-Integration---Emacs%2C-Cursive%2C-etc.). 157 | 158 | ## Contributing 159 | 160 | Currently Krell is only taking bug reports. If you find a bug or would like 161 | to see an enhancement that aligns with the following design principles 162 | please file a Github issue! 163 | 164 | #### Design Principles 165 | 166 | * No ClojureScript React library integration templating. The documentation 167 | should make it pretty clear how to integrate with any particular ClojureScript 168 | React library. 169 | 170 | * Basic React Native only, we're not interested in Expo or any other similar 171 | tooling 172 | 173 | ## License ## 174 | 175 | Copyright (c) Vouch, Inc. All rights reserved. The use and 176 | distribution terms for this software are covered by the Eclipse 177 | Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 178 | which can be found in the file epl-v10.html at the root of this 179 | distribution. By using this software in any fashion, you are 180 | agreeing to be bound by the terms of this license. You must 181 | not remove this notice, or any other, from this software. 182 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {io.methvin/directory-watcher {:mvn/version "0.15.0"} 3 | org.clojure/clojurescript {:mvn/version "1.10.844"} 4 | org.clojure/data.json {:mvn/version "2.5.0"}} 5 | :aliases {:repl {:extra-paths ["test"] 6 | :extra-deps {org.slf4j/slf4j-simple {:mvn/version "1.7.30"}}} 7 | :jar {:replace-deps {com.github.seancorfield/depstar {:mvn/version "2.1.245"}} 8 | :exec-fn hf.depstar/jar 9 | :exec-args {}} 10 | :deploy {:extra-deps {slipset/deps-deploy {:mvn/version "RELEASE"}} 11 | :exec-fn deps-deploy.deps-deploy/deploy 12 | :exec-args {:installer :remote 13 | :sign-releases? true 14 | :artifact "krell.jar"}}}} 15 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | jar 5 | io.vouch 6 | krell 7 | 0.5.4 8 | krell 9 | 10 | 11 | org.clojure 12 | clojure 13 | 1.10.3 14 | 15 | 16 | org.clojure 17 | clojurescript 18 | 1.10.844 19 | 20 | 21 | io.methvin 22 | directory-watcher 23 | 0.15.0 24 | 25 | 26 | 27 | src 28 | 29 | 30 | 31 | clojars 32 | https://repo.clojars.org/ 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /resources/closure_bootstrap.js: -------------------------------------------------------------------------------- 1 | import { npmDeps } from "./npm_deps.js"; 2 | import { krellNpmDeps } from "./krell_npm_deps.js"; 3 | import { assets } from "./krell_assets.js"; 4 | 5 | var METRO_IP = "$METRO_SERVER_IP"; 6 | var METRO_PORT = $METRO_SERVER_PORT; 7 | 8 | const evaluate = eval; 9 | 10 | global.CLOSURE_BASE_PATH = "$CLOSURE_BASE_PATH"; 11 | global.CLOSURE_NO_DEPS = true; 12 | 13 | var loadQueue = []; 14 | var isLoading = false; 15 | var libLoadListeners = {}; 16 | 17 | const notifyListeners = (path) => { 18 | let xs = libLoadListeners[path] || []; 19 | xs.forEach(function (x) { 20 | x(); 21 | }); 22 | libLoadListeners[path] = []; 23 | }; 24 | 25 | const onSourceLoad = (path, cb) => { 26 | if(typeof libLoadListeners[path] === "undefined") { 27 | libLoadListeners[path] = []; 28 | } 29 | libLoadListeners[path].push(cb); 30 | }; 31 | 32 | const loadFile = (path) => { 33 | let url = "http://" + METRO_IP + ":" + METRO_PORT + "/" + path; 34 | return fetch(url) 35 | .then(response => response.text()) 36 | .then(function(source) { 37 | try { 38 | evaluate(source); 39 | setTimeout(() => notifyListeners(path), 0); 40 | } catch(err) { 41 | console.error("Could not evaluate", url, err); 42 | } 43 | }) 44 | .catch(function(err) { 45 | console.error("Could not load", url, err); 46 | }); 47 | }; 48 | 49 | const loadPending = async () => { 50 | for(;;) { 51 | if(loadQueue.length === 0) { 52 | break; 53 | } 54 | let next = loadQueue.shift(); 55 | await loadFile(next); 56 | } 57 | isLoading = false; 58 | }; 59 | 60 | const queueLoad = (path) => { 61 | loadQueue.push(path); 62 | if(!isLoading) { 63 | isLoading = true; 64 | setTimeout(loadPending, 250); 65 | } 66 | }; 67 | 68 | // ============================================================================= 69 | // Bootstrap Files 70 | 71 | evaluate($CLOSURE_BASE_JS); 72 | evaluate($CLOSURE_DEPS_JS); 73 | 74 | // ============================================================================= 75 | // ClojureScript Dev Dependency Graph 76 | 77 | evaluate($CLJS_DEPS_JS); 78 | 79 | // ============================================================================= 80 | // Closure Load Customization and Monkey-patching 81 | 82 | // NOTE: CLOSURE_LOAD_FILE_SYNC not needed as ClojureScript now transpiles 83 | // offending goog.module files that would need runtime transpiler support 84 | global.CLOSURE_IMPORT_SCRIPT = function(path, optContents) { 85 | if (optContents) { 86 | try { 87 | evaluate(optContents); 88 | } catch (e) { 89 | console.error("Could not eval ", path, ":", e); 90 | throw e; 91 | } 92 | return true; 93 | } else { 94 | queueLoad(path); 95 | return true; 96 | } 97 | }; 98 | 99 | global.require = function(x) { 100 | return npmDeps[x] || krellNpmDeps[x] || assets[x]; 101 | }; 102 | 103 | // should be called after the main namespace is loaded 104 | function bootstrapRepl(socket) { 105 | // patch goog.isProvided to allow reloading namespaces at the REPL 106 | if(!goog.isProvided__) goog.isProvided__ = goog.isProvided_; 107 | goog.isProvided_ = (x) => false; 108 | if(!goog.require__) goog.require__ = goog.require; 109 | goog.require = (src, reload) => { 110 | if(reload === "reload-all") { 111 | goog.cljsReloadAll_ = true 112 | } 113 | if(reload || goog.cljsReloadAll_) { 114 | if(goog.debugLoader_) { 115 | let path = goog.debugLoader_.getPathFromDeps_(src); 116 | goog.object.remove(goog.debugLoader_.written_, path); 117 | goog.object.remove(goog.debugLoader_.written_, goog.basePath + path);; 118 | } else { 119 | let path = goog.object.get(goog.dependencies_.nameToPath, src); 120 | goog.object.remove(goog.dependencies_.visited, path); 121 | goog.object.remove(goog.dependencies_.written, path); 122 | goog.object.remove(goog.dependencies_.visited, goog.basePath + path); 123 | } 124 | } 125 | let ret = goog.require__(src); 126 | if(reload === "reload-all") { 127 | goog.cljsReloadAll_ = false 128 | } 129 | if(goog.isInModuleLoader_()) { 130 | return goog.module.getInternal_(src); 131 | } else { 132 | return ret; 133 | } 134 | }; 135 | 136 | // enable printing 137 | cljs.core.enable_console_print_BANG_(); 138 | cljs.core._STAR_print_newline_STAR_ = true; 139 | cljs.core._STAR_print_fn_STAR_ = (str) => { 140 | socket.write(JSON.stringify({ 141 | type: "out", 142 | value: str 143 | })); 144 | socket.write("\0"); 145 | }; 146 | cljs.core._STAR_print_err_fn_STAR_ = (str) => { 147 | socket.write(JSON.stringify({ 148 | type: "err", 149 | value: str 150 | })); 151 | socket.write("\0"); 152 | }; 153 | } 154 | 155 | module.exports = { 156 | evaluate: evaluate, 157 | loadFile: loadFile, 158 | bootstrapRepl: bootstrapRepl, 159 | onSourceLoad: onSourceLoad 160 | }; 161 | -------------------------------------------------------------------------------- /resources/index.js: -------------------------------------------------------------------------------- 1 | /* Generated by Krell, do not edit by hand */ 2 | 3 | import React, { useEffect, useReducer } from "react"; 4 | import { 5 | AppRegistry, 6 | View, 7 | Text 8 | } from 'react-native'; 9 | import {name as appName} from './app.json'; 10 | import {krellUpdateRoot, onKrellReload} from './$KRELL_OUTPUT_TO'; 11 | 12 | let plainStyle = { 13 | flex: 1, 14 | alignItems: 'center', 15 | justifyContent: 'center' 16 | }; 17 | 18 | /* 19 | * Establish a root that works for REPL based dev / prod. In the REPL case 20 | * the real app root will be loaded async. In prod it won't really be async 21 | * but we want to treat both cases the same. 22 | */ 23 | const KrellRoot = (props) => { 24 | const reducer = (state, action) => { 25 | switch(action.type) { 26 | case "updateRoot": 27 | return {...state, loaded: true, root: action.value}; 28 | case "reload": 29 | return {...state, tx: state.tx+1}; 30 | } 31 | }; 32 | const [state, dispatch] = useReducer(reducer, {loaded: false, root: null, tx:0}); 33 | 34 | useEffect(() => { 35 | // Mounting the app the first time 36 | krellUpdateRoot((appRoot) => { 37 | dispatch({type: "updateRoot", value:appRoot}) ; 38 | }); 39 | // Hot reloading 40 | onKrellReload(() => { 41 | dispatch({type: "reload"}); 42 | }); 43 | }, []); 44 | 45 | if(state.loaded) { 46 | return state.root(props); 47 | } else { 48 | return ( 49 | 50 | Waiting for Krell to load files. 51 | 52 | ); 53 | } 54 | } 55 | 56 | AppRegistry.registerComponent(appName, () => KrellRoot); 57 | -------------------------------------------------------------------------------- /resources/krell_repl.js: -------------------------------------------------------------------------------- 1 | import { Platform } from "react-native"; 2 | import TcpSocket from "react-native-tcp-socket"; 3 | import DeviceInfo from "react-native-device-info"; 4 | import { krellPortMap } from '../app.json'; 5 | import { 6 | bootstrapRepl, 7 | evaluate, 8 | loadFile, 9 | onSourceLoad 10 | } from "./closure_bootstrap.js"; 11 | 12 | var CONNECTED = false; 13 | var RECONNECT_INTERVAL = 3000; 14 | 15 | var SERVER_IP = "$KRELL_SERVER_IP"; 16 | var SERVER_PORT = krellPortMap ? krellPortMap[DeviceInfo.getDeviceId()] || $KRELL_SERVER_PORT : $KRELL_SERVER_PORT; 17 | 18 | const KRELL_VERBOSE = $KRELL_VERBOSE; 19 | 20 | var reloadListeners = []; 21 | 22 | console.log("Krell sez howdy, Device ID:", DeviceInfo.getDeviceId()); 23 | 24 | // ============================================================================= 25 | // REPL Server 26 | 27 | const exists_ = (obj, xs) => { 28 | if(typeof xs == "string") { 29 | xs = xs.split("."); 30 | } 31 | if(xs.length >= 1) { 32 | let key = xs[0], 33 | hasKey = obj.hasOwnProperty(key); 34 | if (xs.length === 1) { 35 | return hasKey; 36 | } else { 37 | if(hasKey) { 38 | return exists_(obj[key], xs.slice(1)); 39 | } 40 | } 41 | return false; 42 | } else { 43 | return false; 44 | } 45 | }; 46 | 47 | const notifyReloadListeners = () => { 48 | reloadListeners.forEach(function(x) { 49 | x(); 50 | }); 51 | }; 52 | 53 | const onKrellReload = (cb) => { 54 | reloadListeners.push(cb); 55 | }; 56 | 57 | const errString = (err) => { 58 | if(typeof cljs !== "undefined") { 59 | if(typeof cljs.repl !== "undefined") { 60 | cljs.repl.error__GT_str(err) 61 | } else { 62 | return err.toString(); 63 | } 64 | } else { 65 | return err.toString(); 66 | } 67 | }; 68 | 69 | const handleMessage = (socket, data) => { 70 | var err = null, 71 | ret = null, 72 | msg = null; 73 | 74 | data = data.replace(/\0/g, ""); 75 | 76 | try { 77 | msg = JSON.parse(data); 78 | if(msg.form) { 79 | ret = evaluate(msg.form); 80 | } 81 | } catch (e) { 82 | if(msg && msg.form) { 83 | console.error("Could not evaluate form:", msg.form, e); 84 | } else { 85 | console.error("Invalid message:", msg, e); 86 | } 87 | err = e; 88 | } 89 | 90 | if (err) { 91 | socket.write( 92 | JSON.stringify({ 93 | type: "result", 94 | status: "exception", 95 | value: errString(err) 96 | })+"\0" 97 | ); 98 | } else { 99 | if (ret !== undefined && ret !== null) { 100 | socket.write( 101 | JSON.stringify({ 102 | type: "result", 103 | status: "success", 104 | value: ret.toString(), 105 | })+"\0" 106 | ); 107 | } else { 108 | socket.write( 109 | JSON.stringify({ 110 | type: "result", 111 | status: "success", 112 | value: null, 113 | })+"\0" 114 | ); 115 | } 116 | } 117 | }; 118 | 119 | global.KRELL_RELOAD = async function(nses) { 120 | for(let ns of nses) { 121 | let path = goog.debugLoader_.getPathFromDeps_(ns); 122 | await loadFile(path); 123 | console.log("Reloaded", ns); 124 | } 125 | notifyReloadListeners(); 126 | } 127 | 128 | const initSocket = (socket) => { 129 | var buffer = ""; 130 | 131 | socket.on("data", data => { 132 | if (data[data.length - 1] !== 0) { 133 | buffer += data; 134 | } else { 135 | data = buffer + data; 136 | buffer = ""; 137 | handleMessage(socket, data); 138 | } 139 | }); 140 | 141 | socket.on("error", error => { 142 | socket.destroy(); 143 | //console.log("An error occurred with client socket:", error); 144 | }); 145 | 146 | socket.on("close", error => { 147 | socket.destroy(); 148 | if (CONNECTED) { 149 | console.log("Closed connection with", socket.address()); 150 | } 151 | CONNECTED = false; 152 | setTimeout(tryConnection, RECONNECT_INTERVAL); 153 | }); 154 | }; 155 | 156 | // Loop to connect from client to server 157 | const tryConnection = () => { 158 | if(!CONNECTED) { 159 | var socket = TcpSocket.createConnection({ 160 | host: SERVER_IP, 161 | port: SERVER_PORT, 162 | tls: false, 163 | }, function(address) { 164 | console.log("Connected to Krell REPL Server"); 165 | CONNECTED = true; 166 | // once cljs.core loaded, can monkey-patch Closure and setup printing 167 | if(exists_(global, "cljs.core")) { 168 | bootstrapRepl(socket); 169 | } else { 170 | onSourceLoad(goog.debugLoader_.getPathFromDeps_("cljs.core"), () => { 171 | bootstrapRepl(socket); 172 | }); 173 | } 174 | }); 175 | initSocket(socket); 176 | } else { 177 | setTimeout(tryConnection, RECONNECT_INTERVAL); 178 | } 179 | }; 180 | 181 | if(!process.env["KRELL_NO_REPL"]) { 182 | tryConnection(); 183 | } 184 | 185 | module.exports = { 186 | evaluate: evaluate, 187 | onKrellReload: onKrellReload, 188 | onSourceLoad: onSourceLoad 189 | }; 190 | -------------------------------------------------------------------------------- /resources/main.dev.js: -------------------------------------------------------------------------------- 1 | import { 2 | onSourceLoad, 3 | onKrellReload, 4 | } from './krell_repl.js'; 5 | 6 | var main = '$KRELL_MAIN_NS'; 7 | 8 | function getIn(obj, xs) { 9 | if(obj == null) { 10 | return null; 11 | } else if(xs.length === 0) { 12 | return obj; 13 | } else { 14 | return getIn(obj[xs[0]], xs.slice(1)); 15 | } 16 | } 17 | 18 | function getMainFn(ns) { 19 | let xs = ns.split(".").concat(["_main"]), 20 | fn = getIn(global, xs); 21 | if(fn) { 22 | return fn; 23 | } else { 24 | throw new Error("Could not find -main fn in namespace " + ns); 25 | } 26 | } 27 | 28 | function krellUpdateRoot(cb) { 29 | let path = goog.debugLoader_.getPathFromDeps_(main); 30 | onSourceLoad(path, function () { 31 | cb((props) => { 32 | return getMainFn(main)(props); 33 | }); 34 | }); 35 | global.CLOSURE_UNCOMPILED_DEFINES = $CLOSURE_DEFINES; 36 | $CLJS_PRELOADS 37 | goog.require(main); 38 | } 39 | 40 | module.exports = { 41 | krellUpdateRoot: krellUpdateRoot, 42 | onKrellReload: onKrellReload 43 | }; 44 | -------------------------------------------------------------------------------- /resources/main.prod.js: -------------------------------------------------------------------------------- 1 | function krellUpdateRoot(cb) { 2 | cb($KRELL_MAIN_NS._main); 3 | } 4 | 5 | module.exports = { 6 | krellStaleRoot: function(cb) {}, 7 | krellUpdateRoot: krellUpdateRoot, 8 | onKrellReload: function(cb) {} 9 | }; 10 | -------------------------------------------------------------------------------- /src/deps.cljs: -------------------------------------------------------------------------------- 1 | {:npm-deps {"react-native-tcp-socket" "6.0.6" 2 | "react-native-device-info" "8.1.2"}} 3 | -------------------------------------------------------------------------------- /src/krell/api.clj: -------------------------------------------------------------------------------- 1 | (ns krell.api 2 | (:require [clojure.edn :as edn] 3 | [clojure.java.io :as io] 4 | [krell.repl :as krell])) 5 | 6 | (defn- build* [options] 7 | (krell/krell-compile krell/repl-env {:options options})) 8 | 9 | (defn normalize-ids [x] 10 | (if (sequential? x) 11 | (into [] (map normalize-ids x)) 12 | (cond 13 | (keyword? x) (recur (str (name x) ".edn")) 14 | (symbol? x) (recur (str (name x) ".edn")) 15 | (string? x) (let [rsc (or (some-> x io/resource) 16 | (io/file x))] 17 | (if-not (nil? rsc) 18 | (edn/read-string (slurp rsc)) 19 | (throw 20 | (ex-info (str "Invalid build id: " x) {})))) 21 | (map? x) x 22 | :else 23 | (throw 24 | (ex-info (str "Invalid build id: " x) {}))))) 25 | 26 | (defn build 27 | "Run a Krell build. ids can be a symbol, keyword, string, or map. If 28 | symbol or keyword the classpath will be searched for .edn config file 29 | with a matching name. If a string, must be a relative file path." 30 | ([& ids] 31 | (build* (apply merge (normalize-ids ids))))) 32 | -------------------------------------------------------------------------------- /src/krell/assets.clj: -------------------------------------------------------------------------------- 1 | (ns krell.assets 2 | (:require [clojure.string :as string] 3 | [krell.util :as util])) 4 | 5 | (defn js? [f] 6 | (#{".js"} (util/file-ext f))) 7 | 8 | (defn clojure? [f] 9 | (#{".clj" ".cljc" ".cljs"} (util/file-ext f))) 10 | 11 | (defn asset-require [path] 12 | (str "\"" path "\": require('" path "')" )) 13 | 14 | (defn assets-js [assets] 15 | (str 16 | "module.exports = {\n" 17 | " assets: {\n" 18 | (string/join ",\n" (map (comp #(str " " %) asset-require) assets)) 19 | " }\n" 20 | "};\n")) 21 | -------------------------------------------------------------------------------- /src/krell/deps.clj: -------------------------------------------------------------------------------- 1 | (ns krell.deps 2 | (:require [cljs.analyzer.api :as ana-api] 3 | [cljs.compiler.api :as comp-api] 4 | [cljs.build.api :as build-api] 5 | [cljs.closure :as closure] 6 | [cljs.module-graph :as mg] 7 | [cljs.repl :as repl] 8 | [clojure.java.io :as io] 9 | [krell.util :as util]) 10 | (:import [java.io File])) 11 | 12 | (defn all-deps 13 | "Returns a unsorted sequence of all dependencies for a namespace." 14 | [state ns opts] 15 | (let [ijs (mg/normalize-input (repl/ns->input ns opts))] 16 | (ana-api/with-state state 17 | (map mg/normalize-input 18 | (closure/add-js-sources 19 | (build-api/add-dependency-sources state [ijs] opts) 20 | opts))))) 21 | 22 | (defn deps->graph 23 | "Given a sequence of namespace descriptor maps, returns a map representing 24 | the dependency graph. Because some libraries can have multiple provides the 25 | entries will often represent the same dependency. Deduplication may be 26 | required." 27 | [deps] 28 | (reduce 29 | (fn [acc dep] 30 | (reduce 31 | (fn [acc provide] 32 | (assoc acc provide dep)) 33 | acc (:provides dep))) 34 | {} deps)) 35 | 36 | (defn topo-sort 37 | "Give a dep graph return the topologically sorted sequence of inputs." 38 | [graph] 39 | (let [sorted-keys (mg/topo-sort graph :requires)] 40 | (distinct (map graph sorted-keys)))) 41 | 42 | (defn sorted-deps 43 | "Given a compiler state, a ns symbol, and ClojureScript compiler options, 44 | return a topologically sorted sequence of all the dependencies." 45 | [state ns opts] 46 | (let [all (all-deps state ns opts) 47 | graph (deps->graph all)] 48 | (topo-sort graph))) 49 | 50 | (defn get-out-file ^File [dep opts] 51 | (io/file 52 | (if (:ns dep) 53 | (build-api/src-file->target-file (:source-file dep) opts) 54 | (io/file (:output-dir opts) (closure/rel-output-path dep))))) 55 | 56 | (defn add-out-file [dep opts] 57 | (let [out-file (get-out-file dep opts)] 58 | (merge dep 59 | {:out-file out-file} 60 | (when (.exists out-file) 61 | {:modified (util/last-modified out-file)})))) 62 | 63 | (defn with-out-files 64 | "Given a list of deps return a new list of deps with :out-file property 65 | on each value." 66 | [deps opts] 67 | (into [] (map #(add-out-file % opts)) deps)) 68 | 69 | (defn ^:dynamic dependents* 70 | ([ns graph] 71 | (dependents* ns graph :direct)) 72 | ([ns graph mode] 73 | (let [graph' (->> (filter 74 | (fn [[k v]] 75 | (some #{ns} (:requires v))) 76 | graph) 77 | (into {}))] 78 | (condp = mode 79 | :direct graph' 80 | 81 | :all 82 | (reduce 83 | (fn [ret x] 84 | (merge ret (dependents* x graph))) 85 | graph' (keys graph')) 86 | 87 | (throw (ex-info (str "Unsupported :mode, " mode) {})))))) 88 | 89 | (defn dependents 90 | "Given an ns symbol and a dependency graph return a topologically sorted 91 | sequence of all ancestors." 92 | ([ns graph] 93 | (dependents ns graph :direct)) 94 | ([ns graph mode] 95 | (topo-sort 96 | (binding [dependents* (memoize dependents*)] 97 | (dependents* (-> ns comp-api/munge str) graph mode))))) 98 | -------------------------------------------------------------------------------- /src/krell/gen.clj: -------------------------------------------------------------------------------- 1 | (ns krell.gen 2 | (:require [cljs.compiler.api :as comp-api] 3 | [clojure.data.json :as json] 4 | [clojure.java.io :as io] 5 | [clojure.string :as string] 6 | [krell.assets :as assets] 7 | [krell.net :as net] 8 | [krell.util :as util]) 9 | (:import [java.io File])) 10 | 11 | (defn contents-equal? [f content] 12 | (= (slurp f) content)) 13 | 14 | (defn write-if-different [^File f content] 15 | (when-not (and (.exists f) 16 | (contents-equal? f content)) 17 | (spit f content))) 18 | 19 | (defn write-index-js 20 | "Write the Krell index.js file which bootstraps the Krell application. 21 | See resources/index.js" 22 | [repl-env opts] 23 | (let [source (slurp (or (io/resource (get-in repl-env [:options :index-js] "krell_index.js")) 24 | (io/resource "index.js"))) 25 | out-file (io/file "index.js")] 26 | ;; TODO: just writing this out to the top level, can we allow this to be 27 | ;; in a different location? 28 | (util/mkdirs out-file) 29 | (write-if-different out-file 30 | (-> source 31 | (string/replace "$KRELL_OUTPUT_TO" (:output-to opts)) 32 | (string/replace "$KRELL_OUTPUT_DIR" (:output-dir opts)))))) 33 | 34 | (defn write-closure-bootstrap 35 | [repl-env opts] 36 | (let [source (slurp (io/resource "closure_bootstrap.js")) 37 | goog-base (slurp (io/resource "goog/base.js")) 38 | goog-deps (slurp (io/resource "goog/deps.js")) 39 | cljs-deps (slurp (io/file (:output-dir opts) "cljs_deps.js")) 40 | out-file (io/file (:output-dir opts) "closure_bootstrap.js")] 41 | (util/mkdirs out-file) 42 | (write-if-different out-file 43 | (-> source 44 | (string/replace "$METRO_SERVER_IP" (get-in repl-env [:options :host] (net/get-ip))) 45 | (string/replace "$METRO_SERVER_PORT" (str (:metro-port opts 8081))) 46 | (string/replace "$CLOSURE_BASE_JS" (pr-str goog-base)) 47 | (string/replace "$CLOSURE_DEPS_JS" (pr-str goog-deps)) 48 | (string/replace "$CLJS_DEPS_JS" (pr-str cljs-deps)) 49 | (string/replace "$CLOSURE_BASE_PATH" 50 | (string/replace 51 | (str (.getPath (io/file (:output-dir opts) "goog")) "/") 52 | File/separator "/")))))) 53 | 54 | (defn write-repl-js 55 | "Write out the REPL support code. See resources/krell_repl.js" 56 | [repl-env opts] 57 | (let [source (slurp (io/resource "krell_repl.js")) 58 | out-file (io/file (:output-dir opts) "krell_repl.js")] 59 | (util/mkdirs out-file) 60 | (write-if-different out-file 61 | (-> source 62 | (string/replace "$KRELL_VERBOSE" (str (or (-> repl-env :options :krell/verbose) false))) 63 | (string/replace "$KRELL_SERVER_IP" (get-in repl-env [:options :host] (net/get-ip))) 64 | (string/replace "$KRELL_SERVER_PORT" (-> repl-env :options :port str)))))) 65 | 66 | (defn write-assets-js 67 | "Write out the REPL asset support code." 68 | [assets opts] 69 | (let [out-file (io/file (:output-dir opts) "krell_assets.js")] 70 | (util/mkdirs out-file) 71 | (write-if-different out-file (assets/assets-js assets)))) 72 | 73 | (defn export-dep [dep] 74 | (str "\""dep "\": require('" dep "')" )) 75 | 76 | (defn krell-npm-deps-js 77 | "Returns the JavaScript code to support runtime require of bundled modules." 78 | [node-requires] 79 | (str 80 | "module.exports = {\n" 81 | " krellNpmDeps: {\n" 82 | (string/join ",\n" (map (comp #(str " " %) export-dep) node-requires)) 83 | " }\n" 84 | "};\n")) 85 | 86 | (defn write-krell-npm-deps-js 87 | [node-requires opts] 88 | (let [out-file (io/file (:output-dir opts) "krell_npm_deps.js")] 89 | (util/mkdirs out-file) 90 | (write-if-different out-file (krell-npm-deps-js node-requires)))) 91 | 92 | (defn goog-require-str [sym] 93 | (str "goog.require(\"" (comp-api/munge sym) "\");")) 94 | 95 | (defn krell-main-js 96 | "Return the source for build dependent entry point. See resources/main.dev.js 97 | and resources/main.prod.js" 98 | [opts] 99 | (let [source (slurp 100 | (if (= :none (:optimizations opts)) 101 | (io/resource "main.dev.js") 102 | (io/resource "main.prod.js")))] 103 | (-> source 104 | (string/replace "$KRELL_MAIN_NS" (str (munge (:main opts)))) 105 | (string/replace "$CLOSURE_DEFINES" (json/write-str (:closure-defines opts))) 106 | (string/replace "$CLJS_PRELOADS" 107 | (string/join "\n" (map goog-require-str (:preloads opts))))))) 108 | -------------------------------------------------------------------------------- /src/krell/main.clj: -------------------------------------------------------------------------------- 1 | (ns krell.main 2 | (:require [cljs.main])) 3 | 4 | ;; TODO: check that either -re is already -re krell.repl or not 5 | ;; supplied 6 | ;; TODO: should we set :analyze-path always? 7 | (defn -main [& args] 8 | (try 9 | (apply cljs.main/-main (concat ["-re" "krell.repl"] args)) 10 | (catch Exception e 11 | (.printStackTrace e)) 12 | (finally 13 | ;; TODO: shouldn't need this 14 | (System/exit 0)))) 15 | -------------------------------------------------------------------------------- /src/krell/net.clj: -------------------------------------------------------------------------------- 1 | (ns krell.net 2 | (:require [clojure.java.io :as io]) 3 | (:import [java.io BufferedReader BufferedWriter IOException] 4 | [java.net InetAddress Inet4Address NetworkInterface ServerSocket Socket])) 5 | 6 | (defn create-server-socket ^ServerSocket [port] 7 | (ServerSocket. port)) 8 | 9 | (defn socket->socket-map [socket] 10 | (let [in (io/reader socket) 11 | out (io/writer socket)] 12 | {:socket socket :in in :out out})) 13 | 14 | (defn create-socket [^String host port] 15 | (socket->socket-map (Socket. host (int port)))) 16 | 17 | (defn close-socket [s] 18 | (.close (:socket s)) 19 | (.close (:in s)) 20 | (.close (:out s))) 21 | 22 | (defn write [^BufferedWriter out ^String js] 23 | (.write out js) 24 | (.write out (int 0)) ;; terminator 25 | (.flush out)) 26 | 27 | (defn ^String read-response [^BufferedReader in] 28 | (let [sb (StringBuilder.)] 29 | (loop [sb sb c (.read in)] 30 | (case c 31 | -1 (throw (IOException. "Stream closed")) 32 | 0 (str sb) 33 | (do 34 | (.append sb (char c)) 35 | (recur sb (.read in))))))) 36 | 37 | (defn get-ip [] 38 | (-> (filter 39 | (fn [^InetAddress ia] 40 | (and (not (.isLinkLocalAddress ia)) 41 | (not (.isLoopbackAddress ia)) 42 | (instance? Inet4Address ia))) 43 | (mapcat 44 | (fn [^NetworkInterface ni] 45 | (when (.isUp ni) 46 | (enumeration-seq (.getInetAddresses ni)))) 47 | (enumeration-seq (NetworkInterface/getNetworkInterfaces)))) 48 | first (.getHostAddress))) 49 | -------------------------------------------------------------------------------- /src/krell/passes.clj: -------------------------------------------------------------------------------- 1 | (ns krell.passes 2 | (:require [cljs.analyzer :as ana] 3 | [cljs.analyzer.api :as ana-api] 4 | [clojure.edn :as edn] 5 | [clojure.java.io :as io] 6 | [clojure.string :as string] 7 | [krell.assets :as assets] 8 | [krell.util :as util]) 9 | (:import [java.io File])) 10 | 11 | (def ^:dynamic *nses-with-requires* nil) 12 | 13 | (defn normalize [s] 14 | (cond-> s (string/starts-with? s "./") (subs 2))) 15 | 16 | (defn asset? [s] 17 | (and (not (nil? (util/file-ext s))) 18 | (not (assets/js? s)))) 19 | 20 | (defn lib? [s] 21 | (not (asset? s))) 22 | 23 | (defn js-require? [ast] 24 | (and (= :invoke (:op ast)) 25 | (= 'js/require (-> ast :fn :name)) 26 | (= :const (-> ast :args first :op)))) 27 | 28 | (defn js-require-asset? [ast] 29 | (and (js-require? ast) 30 | (asset? (-> ast :args first :val)))) 31 | 32 | (defn update-require-path [ast new-path] 33 | (update-in ast [:args 0] merge 34 | {:val new-path :form new-path})) 35 | 36 | (defn rewrite-asset-requires [env ast opts] 37 | (if (js-require-asset? ast) 38 | (let [new-path 39 | (let [ns (-> env :ns :name)] 40 | (string/replace 41 | (util/get-path 42 | (util/relativize 43 | (.getAbsoluteFile (io/file (:output-dir opts))) 44 | (.getAbsoluteFile 45 | (io/file 46 | (.getParentFile (io/file (ana-api/current-file))) 47 | (normalize (-> ast :args first :val)))))) 48 | File/separator "/")) 49 | cur-ns (ana-api/current-ns)] 50 | (when *nses-with-requires* 51 | (swap! *nses-with-requires* conj cur-ns)) 52 | (swap! (ana-api/current-state) update-in 53 | [::ana/namespaces cur-ns ::assets] (fnil conj #{}) new-path) 54 | (update-require-path ast new-path)) 55 | ast)) 56 | 57 | (defn js-require-lib? [ast] 58 | (and (js-require? ast) 59 | (lib? (-> ast :args first :val)))) 60 | 61 | (defn collect-lib-requires [env ast opts] 62 | (when (js-require-lib? ast) 63 | (let [lib (-> ast :args first :val) 64 | cur-ns (ana-api/current-ns)] 65 | (when *nses-with-requires* 66 | (swap! *nses-with-requires* conj cur-ns)) 67 | (swap! (ana-api/current-state) update-in 68 | [::ana/namespaces cur-ns ::requires] (fnil conj #{}) lib))) 69 | ast) 70 | 71 | (defn cache-krell-requires [nses opts] 72 | ;; NOTE: just additive for now, can revisit later if someone finds a 73 | ;; performance issue 74 | (let [out-file (io/file (:output-dir opts) "krell_requires.edn") 75 | nses' (cond-> nses 76 | (.exists out-file) 77 | (into (edn/read-string (slurp out-file))))] 78 | (util/mkdirs out-file) 79 | (spit out-file (pr-str nses')) 80 | nses')) 81 | 82 | (defn load-analysis [nses opts] 83 | (reduce 84 | (fn [ret ns] 85 | (assoc-in ret [::ana/namespaces ns] 86 | (ana-api/read-analysis-cache (util/ns->cache-file ns opts)))) 87 | {} nses)) 88 | 89 | (defn all-assets [analysis] 90 | (into #{} (mapcat (comp ::assets val) (get-in analysis [::ana/namespaces])))) 91 | 92 | (defn all-requires [analysis] 93 | (into #{} (mapcat (comp ::requires val) (get-in analysis [::ana/namespaces])))) 94 | 95 | (def custom-passes [rewrite-asset-requires collect-lib-requires]) 96 | -------------------------------------------------------------------------------- /src/krell/repl.clj: -------------------------------------------------------------------------------- 1 | (ns krell.repl 2 | (:require [cljs.analyzer.api :as ana-api] 3 | [cljs.build.api :as build-api] 4 | [cljs.cli :as cli] 5 | [cljs.compiler.api :as comp-api] 6 | [cljs.repl :as repl] 7 | [clojure.data.json :as json] 8 | [clojure.java.io :as io] 9 | [clojure.string :as string] 10 | [krell.deps :as deps] 11 | [krell.gen :as gen] 12 | [krell.net :as net] 13 | [krell.passes :as passes] 14 | [krell.util :as util] 15 | [krell.watcher :as watcher]) 16 | (:import [java.io File IOException] 17 | [java.util.concurrent LinkedBlockingQueue])) 18 | 19 | (def eval-lock (Object.)) 20 | (def results-queue (LinkedBlockingQueue.)) 21 | 22 | (defn rn-eval 23 | "Evaluate a JavaScript string in the React Native REPL" 24 | [repl-env js] 25 | (locking eval-lock 26 | (let [{:keys [out]} @(:socket repl-env)] 27 | (net/write out 28 | (json/write-str {:type "eval" :form js})) 29 | (let [result (.take results-queue) 30 | ret (condp = (:status result) 31 | "success" 32 | {:status :success 33 | :value (:value result)} 34 | 35 | "exception" 36 | {:status :exception 37 | :value (:value result)} 38 | (throw 39 | (ex-info 40 | (str "Unexpected message type: " 41 | (pr-str (:status result))) 42 | {:queue-value result})))] 43 | ret)))) 44 | 45 | (defn load-javascript 46 | "Load a Closure JavaScript file into the React Native REPL" 47 | [repl-env provides url] 48 | (rn-eval repl-env (slurp url))) 49 | 50 | (defn event-loop 51 | "Event loop that listens for responses from the client." 52 | [{:keys [state socket] :as repl-env}] 53 | (while (not (:done @state)) 54 | (try 55 | (let [res (net/read-response (:in @socket))] 56 | (try 57 | (let [{:keys [type value] :as event} 58 | (json/read-str res :key-fn keyword)] 59 | (case type 60 | "result" (.offer results-queue event) 61 | (when-let [stream (if (= type "out") *out* *err*)] 62 | (.write stream value 0 (.length ^String value)) 63 | (.flush stream)))) 64 | (catch Throwable _ 65 | (.write *out* res 0 (.length res)) 66 | (.flush *out*)))) 67 | (catch IOException e 68 | ;; TODO: we should probably log something here 69 | (Thread/sleep 500))))) 70 | 71 | (defn modified-source? 72 | [{:keys [file-index] :as repl-env} {:keys [type path]}] 73 | (or (= :modify type) 74 | (and (= :create type) 75 | (let [f (.getAbsoluteFile (util/to-file path))] 76 | (and (not (.isDirectory f)) 77 | (if-let [t (get @file-index f)] 78 | (< t (util/last-modified f)) 79 | (boolean (swap! file-index assoc f (util/last-modified f))))))))) 80 | 81 | (defn collecting-warning-handler [state] 82 | (fn [warn-type env info] 83 | (when (warn-type (ana-api/enabled-warnings)) 84 | (let [msg (str (ana-api/warning-message warn-type info) 85 | (when-let [line (:line env)] (str " at line " line)))] 86 | (swap! state conj msg))))) 87 | 88 | (defn warn-client [repl-env s] 89 | (rn-eval repl-env (str "console.warn(" (pr-str s) ")"))) 90 | 91 | (defn recompile 92 | "Recompile the ClojureScript file specified by :path key in the first 93 | parameter. This is called by the watcher off the main thread." 94 | [repl-env {:keys [path] :as evt} opts] 95 | (when-not (:main opts) 96 | (throw (ex-info (str ":main namespace not supplied in build configuration") 97 | {:krell/error :main-ns-missing}))) 98 | (let [reloads (atom []) 99 | src (util/to-file path) 100 | path-str (.getPath src)] 101 | (when (and (< 0 (.length src)) ;; ignore newly created files 102 | (modified-source? repl-env evt) 103 | (#{".cljc" ".cljs"} (subs path-str (.lastIndexOf path-str ".")))) 104 | (try 105 | (let [state (ana-api/current-state) 106 | ns-info (ana-api/parse-ns src) 107 | the-ns (:ns ns-info) 108 | ancs (deps/dependents the-ns 109 | (deps/deps->graph 110 | (deps/all-deps state (:main opts) opts)) 111 | (-> repl-env :options :recompile)) 112 | all (concat [ns-info] ancs)] 113 | (try 114 | ;; we need to compute js deps so that requires from node_modules won't fail 115 | (build-api/handle-js-modules state 116 | (build-api/dependency-order 117 | (build-api/add-dependency-sources all opts)) 118 | opts) 119 | (loop [xs all deps-js ""] 120 | (if-let [ijs (first xs)] 121 | (let [warns (atom []) 122 | handler (collecting-warning-handler warns) 123 | ijs' (try 124 | (ana-api/with-warning-handlers [handler] 125 | (ana-api/with-passes 126 | (into ana-api/default-passes passes/custom-passes) 127 | (comp-api/compile-file state 128 | (:source-file ijs) 129 | (build-api/target-file-for-cljs-ns 130 | (:ns ijs) (:output-dir opts)) opts))) 131 | (catch Throwable t 132 | (println t) 133 | (warn-client repl-env 134 | (str (:ns ns-info) 135 | " compilation failed with exception: " 136 | (.getMessage t))) 137 | nil))] 138 | (if (empty? @warns) 139 | (swap! reloads conj (munge (:ns ijs))) 140 | ;; TODO: it may be that warns strings have chars that will break console.warn ? 141 | ;; TODO: also warn at REPL 142 | (let [pre (str "Could not recompile " (:ns ijs) ":")] 143 | (warn-client repl-env 144 | (string/join "\n" (concat [pre] @warns))))) 145 | (recur 146 | (next xs) 147 | ;; dep string must computed from a *compiled* file, 148 | ;; thus ijs' and not ijs 149 | (if (and ijs' (empty? @warns)) 150 | (str deps-js (build-api/goog-dep-string opts ijs')) 151 | deps-js))) 152 | (rn-eval repl-env deps-js))) 153 | ;; have the client queue the reloads 154 | (rn-eval repl-env 155 | (str 156 | (apply str "KRELL_RELOAD([" 157 | (interpose "," (map (comp pr-str str) @reloads))) "])")))) 158 | (catch Throwable t 159 | (println t)))))) 160 | 161 | (defn server-loop 162 | [{:keys [socket state] :as repl-env} server-socket] 163 | (when-let [conn (try (.accept server-socket) (catch Throwable _))] 164 | (.setKeepAlive conn true) 165 | (when-let [sock @socket] 166 | (future (net/close-socket sock))) 167 | (reset! socket (net/socket->socket-map conn)) 168 | (when-not (:done @state) 169 | (recur repl-env server-socket)))) 170 | 171 | (defn setup 172 | ([repl-env] (setup repl-env nil)) 173 | ([{:keys [options state socket] :as repl-env} opts] 174 | (let [port (:port options)] 175 | (println "\nWaiting for device connection on port" port) 176 | (.start 177 | (Thread. 178 | (bound-fn [] 179 | (server-loop repl-env (net/create-server-socket port)))))) 180 | (while (not @socket) 181 | (Thread/sleep 500)) 182 | (.start (Thread. (bound-fn [] (event-loop repl-env)))) 183 | ;; create and start the watcher 184 | (when (-> repl-env :options :recompile) 185 | (swap! state assoc :watcher 186 | (doto 187 | (apply watcher/create 188 | ;; have to pass the processed opts 189 | ;; the compiler one are the original ones 190 | (bound-fn [e] (recompile repl-env e opts)) 191 | (:watch-dirs options)) 192 | (watcher/watch)))) 193 | ;; NOTE: duplicated from recompile above 194 | ;; TODO: should be able to run this on another thread 195 | (when-let [main-ns (:main opts)] 196 | (let [state (ana-api/current-state) 197 | ns-info (ana-api/parse-ns (build-api/ns->source main-ns)) 198 | the-ns (:ns ns-info) 199 | ancs (deps/dependents the-ns 200 | (deps/deps->graph 201 | (deps/all-deps state main-ns opts)) 202 | :direct) 203 | all (concat [ns-info] ancs)] 204 | (build-api/handle-js-modules state 205 | (build-api/dependency-order 206 | (build-api/add-dependency-sources all opts)) 207 | opts))))) 208 | 209 | (defn host-opt 210 | [cfg value] 211 | (assoc-in cfg [:repl-env-options :host] value)) 212 | 213 | (defn port-opt 214 | [cfg value] 215 | (assoc-in cfg [:repl-env-options :port] 216 | (cond-> value 217 | (string? value) Integer/parseInt))) 218 | 219 | (defn recompile-opt 220 | [cfg value] 221 | (assoc-in cfg [:repl-env-options :recompile] 222 | (if (= "false" value) false (keyword value)))) 223 | 224 | (defn custom-index-opt 225 | [cfg value] 226 | (assoc-in cfg [:repl-env-options :index-js] value)) 227 | 228 | (defn watch-dirs-opt 229 | [cfg value] 230 | (assoc-in cfg [:repl-env-options :watch-dirs] 231 | (into [] (string/split value (re-pattern (str File/pathSeparatorChar)))))) 232 | 233 | (defn krell-compile 234 | [repl-env-var {:keys [repl-env-options options] :as cfg}] 235 | (let [repl-env (apply repl-env-var (mapcat identity repl-env-options))] 236 | (gen/write-repl-js repl-env options) 237 | ;; everything that doesn't need analysis needs to be written before the 238 | ;; following as cli/default-compile may invoke the REPL 239 | (let [opt-level (:optimizations options)] 240 | (ana-api/with-passes 241 | (into ana-api/default-passes passes/custom-passes) 242 | (binding [passes/*nses-with-requires* (atom #{})] 243 | (cli/default-compile repl-env-var 244 | (cond-> 245 | (assoc cfg 246 | :post-compile-fn 247 | #(let [nses (passes/cache-krell-requires @passes/*nses-with-requires* options) 248 | analysis (passes/load-analysis nses options)] 249 | (gen/write-assets-js (passes/all-assets analysis) options) 250 | (gen/write-krell-npm-deps-js (passes/all-requires analysis) options) 251 | (when (or (nil? (:optimizations options)) 252 | (= :none (:optimizations options))) 253 | (gen/write-closure-bootstrap repl-env options)) 254 | (gen/write-index-js repl-env options))) 255 | (not (or (= :none opt-level) (nil? opt-level))) 256 | (assoc-in [:options :output-wrapper] 257 | (fn [source] (str source (gen/krell-main-js options))))))))))) 258 | 259 | (defrecord KrellEnv [options file-index socket state] 260 | repl/IReplEnvOptions 261 | (-repl-options [this] 262 | { 263 | :output-dir ".krell_repl" 264 | 265 | ;; RN target defaults 266 | :process-shim false 267 | :target :bundle 268 | :target-fn 'krell.gen/krell-main-js 269 | 270 | ;; cljs.cli extension points 271 | ::repl/fast-initial-prompt? :after-setup 272 | ::cli/commands {:groups 273 | {::cli/main&compile 274 | {:desc "init options" 275 | :pseudos {["-re" "--repl-env"] 276 | {:arg "env" 277 | :doc (str "Defaults to the only supported value - krell.repl")}}}} 278 | :init 279 | {["-t" "--target"] 280 | {:group ::cli/main&compile 281 | :fn (fn [cfg target] 282 | (assert (#{"node" "nodejs"} target) "Invalid --target, only nodejs supported") 283 | cfg) 284 | :arg "name" 285 | :doc (str "The JavaScript target. Supported values: node or nodejs")} 286 | ["-wd" "--watch-dirs"] 287 | {:group ::cli/main 288 | :fn watch-dirs-opt 289 | :arg "files" 290 | :doc (str "A platform separated list of directories to watch for REPL hot-reloading")} 291 | ["-H" "--host"] 292 | {:group ::cli/main 293 | :fn host-opt 294 | :arg "string" 295 | :doc (str "Set host address to connect to")} 296 | ["-p" "--port"] 297 | {:group ::cli/main 298 | :fn port-opt 299 | :arg "number" 300 | :doc (str "Set port for target clients to bind to")} 301 | ["-rc" "--recompile"] 302 | {:group ::cli/main 303 | :fn recompile-opt 304 | :arg "string" 305 | :doc (str "Flag for recompile strategy. Supported values: direct, all, or false." 306 | " If direct, Krell will only recompile namespaces that directly depend" 307 | " on the changed one. If false, Krell will not recompile and reload." 308 | " Defaults to direct")} 309 | ["-I" "--index-js"] 310 | {:group ::cli/main 311 | :fn custom-index-opt 312 | :arg "string" 313 | :doc (str "Custom index.js resource on the classpath, i.e. Expo helpers etc.")}} 314 | :main 315 | {["-s" "--serve"] 316 | {:fn (fn [cfg opt] 317 | (throw "--serve not supported")) 318 | :arg "N/A" 319 | :doc (str "NOT SUPPORTED")}}} 320 | ::cli/compile krell-compile 321 | }) 322 | repl/IParseError 323 | (-parse-error [_ err _] 324 | (assoc err :value nil)) 325 | repl/IJavaScriptEnv 326 | (-setup [this opts] 327 | (setup this opts)) 328 | (-evaluate [this filename line js] 329 | (rn-eval this js)) 330 | (-load [this provides url] 331 | (load-javascript this provides url)) 332 | (-tear-down [this] 333 | (let [sock @socket] 334 | (swap! state assoc :done true) 335 | (when-let [w (:watcher @state)] 336 | (watcher/stop w)) 337 | ;; the socket might have been destroyed by a RN refresh so do this 338 | ;; off the main thread 339 | (future 340 | (when (and (:socket sock) 341 | (not (.isClosed (:socket sock)))) 342 | (net/close-socket sock)))))) 343 | 344 | (defn file-index [dirs] 345 | (reduce 346 | (fn [ret ^File f] 347 | (assoc ret (.getAbsoluteFile f) (util/last-modified f))) 348 | {} (mapcat (comp util/files-seq io/file) dirs))) 349 | 350 | (defn repl-env* [options] 351 | (let [watch-dirs (:watch-dirs options ["src"]) 352 | index (atom (file-index watch-dirs))] 353 | (KrellEnv. 354 | (merge 355 | {:port 5001 356 | :watch-dirs watch-dirs 357 | :connect-timeout 30000 358 | :eval-timeout 30000 359 | :recompile :direct} 360 | options) 361 | index (atom nil) (atom nil)))) 362 | 363 | (defn repl-env 364 | "Construct a React Native evaluation environment." 365 | [& {:as options}] 366 | (repl-env* options)) 367 | 368 | (defn -main [& args] 369 | (apply cli/main repl-env args)) 370 | -------------------------------------------------------------------------------- /src/krell/util.clj: -------------------------------------------------------------------------------- 1 | (ns krell.util 2 | (:require [cljs.build.api :as build-api] 3 | [clojure.java.io :as io] 4 | [clojure.string :as string]) 5 | (:import [java.io File] 6 | [java.net URL] 7 | [java.nio.file Path])) 8 | 9 | (defn now 10 | "Returns System/currentTimeMillis" 11 | ^long [] 12 | (System/currentTimeMillis)) 13 | 14 | (defn elapsed 15 | "Give a long representing some instant in milliseconds, returns elapsed." 16 | ^long [t] 17 | (- (now) t)) 18 | 19 | (defn file? [f] 20 | (instance? File f)) 21 | 22 | (defn url? [f] 23 | (instance? URL f)) 24 | 25 | (defn last-modified [src] 26 | (cond 27 | (file? src) (.lastModified ^File src) 28 | (url? src) 29 | (let [conn (.openConnection ^URL src)] 30 | (try 31 | (.getLastModified conn) 32 | (finally 33 | (let [ins (.getInputStream conn)] 34 | (when ins 35 | (.close ins)))))) 36 | :else 37 | (throw 38 | (IllegalArgumentException. (str "Cannot get last modified for " src))))) 39 | 40 | (defn changed? [a b] 41 | (not (== (last-modified a) (last-modified b)))) 42 | 43 | (defn to-file ^File [^Path path] 44 | (.toFile path)) 45 | 46 | (defn to-path ^Path [^File f] 47 | (.toPath f)) 48 | 49 | (defn relativize ^File [^File source ^File target] 50 | (to-file (.relativize (to-path source) (to-path target)))) 51 | 52 | (defn get-path [^File f] 53 | (.getPath f)) 54 | 55 | (defn file-ext [f] 56 | (let [path (if (file? f) (.getPath ^File f) f)] 57 | (let [idx (.lastIndexOf path ".")] 58 | (when (pos? idx) (subs path idx))))) 59 | 60 | (defn files-seq [dir] 61 | (tree-seq 62 | (fn [^File f] 63 | (. f (isDirectory))) 64 | (fn [^File d] 65 | (seq (. d (listFiles)))) 66 | dir)) 67 | 68 | (defn mkdirs 69 | "Create all parent directories for the passed file." 70 | [^File f] 71 | (.mkdirs (.getParentFile (.getCanonicalFile f)))) 72 | 73 | (defn ns->cache-file [ns {:keys [output-dir] :as opts}] 74 | (let [f (build-api/target-file-for-cljs-ns ns output-dir) 75 | path (-> ns build-api/ns->location :relative-path)] 76 | (io/file (str (string/replace (.getPath f) #".js$" "") (file-ext path) ".cache.json")))) 77 | 78 | (defn closure-relative-path [file-path opts] 79 | (.getPath (relativize (io/file (:output-dir opts) "goog") (io/file file-path)))) 80 | 81 | (defn url-path [f] 82 | (string/replace (get-path f) File/separator "/")) 83 | 84 | (defn platform-path [rel-url-frag] 85 | (string/replace rel-url-frag "/" File/separator)) 86 | -------------------------------------------------------------------------------- /src/krell/watcher.clj: -------------------------------------------------------------------------------- 1 | (ns krell.watcher 2 | (:import [io.methvin.watcher DirectoryChangeEvent DirectoryChangeEvent$EventType 3 | DirectoryChangeListener DirectoryWatcher] 4 | [java.nio.file Paths] 5 | [org.slf4j LoggerFactory])) 6 | 7 | (def logger (LoggerFactory/getLogger "krell")) 8 | 9 | (defn fn->listener ^DirectoryChangeListener [f] 10 | (reify 11 | DirectoryChangeListener 12 | (onEvent [this e] 13 | (let [path (.path ^DirectoryChangeEvent e)] 14 | (condp = (. ^DirectoryChangeEvent e eventType) 15 | DirectoryChangeEvent$EventType/CREATE (f {:type :create :path path}) 16 | DirectoryChangeEvent$EventType/MODIFY (f {:type :modify :path path}) 17 | DirectoryChangeEvent$EventType/DELETE (f {:type :delete :path path}) 18 | DirectoryChangeEvent$EventType/OVERFLOW (f {:type :overflow :path path})))))) 19 | 20 | (defn to-path [& args] 21 | (Paths/get ^String (first args) (into-array String (rest args)))) 22 | 23 | (defn create [cb & paths] 24 | (-> (DirectoryWatcher/builder) 25 | (.paths (map to-path paths)) 26 | (.listener (fn->listener cb)) 27 | (.build))) 28 | 29 | (defn watch [^DirectoryWatcher watcher] 30 | (.watchAsync watcher)) 31 | 32 | (defn stop [^DirectoryWatcher watcher] 33 | (.close watcher)) 34 | 35 | (comment 36 | 37 | (def watcher 38 | (create 39 | (fn [e] 40 | (. logger (info (pr-str e)))) 41 | "src")) 42 | 43 | (watch watcher) 44 | 45 | ) 46 | 47 | -------------------------------------------------------------------------------- /test/krell/test_deps.clj: -------------------------------------------------------------------------------- 1 | (ns krell.test-deps 2 | (:require [cljs.analyzer.api :as ana-api] 3 | [clojure.pprint :refer [pprint]] 4 | [clojure.test :as test :refer [deftest is run-tests]] 5 | [krell.deps :as deps])) 6 | 7 | (deftest test-topo-sort 8 | (let [opts {:output-dir "target"} 9 | state (ana-api/empty-state) 10 | all (deps/all-deps state 'cljs.core opts) 11 | graph (deps/deps->graph all) 12 | sorted (deps/topo-sort graph)] 13 | (is (= 'cljs.core (:ns (last sorted)))))) 14 | 15 | (deftest test-with-out-files 16 | (let [opts {:output-dir "target"} 17 | state (ana-api/empty-state) 18 | all (deps/all-deps state 'cljs.core opts) 19 | all' (deps/with-out-files 20 | (deps/topo-sort (deps/deps->graph all)) 21 | opts)] 22 | (is (== (count all) (count all'))))) 23 | 24 | (deftest test-dependents 25 | (let [opts {:output-dir "target"} 26 | state (ana-api/empty-state) 27 | graph (deps/deps->graph (deps/all-deps state 'cljs.core opts)) 28 | direct (map :provides (deps/dependents 'goog.asserts graph)) 29 | all (map :provides (deps/dependents "goog.asserts" graph :all))] 30 | (is (not (empty? direct))) 31 | (is (= (into #{} direct)) 32 | (->> (filter 33 | (fn [[_ x]] 34 | (some #{"goog.asserts"} (:requires x))) 35 | graph) 36 | (map (comp :provides second)) 37 | (into #{}))) 38 | (is (< (count direct) (count all))) 39 | (is (= ["cljs.core"] (last all))))) 40 | -------------------------------------------------------------------------------- /test/krell/test_paths.clj: -------------------------------------------------------------------------------- 1 | (ns krell.test-paths 2 | (:require [clojure.java.io :as io] 3 | [clojure.test :as test :refer [deftest is run-tests]] 4 | [krell.util :as util])) 5 | 6 | (deftest test-closure-relative-path 7 | (let [{:keys [output-dir] :as opts} {:output-dir "target"}] 8 | (is (= "base.js" 9 | (util/closure-relative-path 10 | (io/file output-dir "goog/base.js") 11 | opts))) 12 | (is (= "object/object.js" 13 | (util/closure-relative-path 14 | (io/file output-dir "goog/object/object.js") 15 | opts))) 16 | (is (= "../cljs/core.js" 17 | (util/closure-relative-path 18 | (io/file output-dir "cljs/core.js") 19 | opts))))) 20 | --------------------------------------------------------------------------------