├── .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 |
--------------------------------------------------------------------------------