├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.js ├── lumo-example ├── README.md ├── package.json ├── serverless.yml └── src │ └── lumo_example │ └── core.cljs ├── package.json └── serverless-cljs-plugin ├── munge.js └── serverless_lumo ├── build.cljs ├── index.cljs └── templates.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | pids 5 | *.pid 6 | *.seed 7 | .grunt 8 | node_modules 9 | .npm 10 | .node_repl_history 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | node_modules 3 | README.md 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serverless-cljs-plugin 2 | 3 | [![npm version](https://badge.fury.io/js/serverless-cljs-plugin.svg)](https://badge.fury.io/js/serverless-cljs-plugin) 4 | 5 | A [Serverless](https://github.com/serverless/serverless) plugin which 6 | uses lein/[cljs-lambda](https://github.com/nervous-systems/cljs-lambda) (or, 7 | optionally [Lumo](https://github.com/anmonteiro/lumo)) to package services 8 | written in [Clojurescript](https://clojurescript.org/). 9 | 10 | ## Leiningen Template 11 | 12 | ``` shell 13 | $ lein new serverless-cljs example 14 | example$ lein deps 15 | ``` 16 | 17 | Will generate an `example` directory containing a minimal `serverless.yml` and 18 | `project.clj` demonstrating this plugin's functionality. 19 | 20 | ### [Guide to using the plugin via Lein/JVM compilation.](https://nervous.io/clojurescript/lambda/2017/02/06/serverless-cljs/) 21 | 22 | ## Usage 23 | 24 | ```yaml 25 | functions: 26 | echo: 27 | cljs: example.core/echo 28 | 29 | plugins: 30 | - serverless-cljs-plugin 31 | ``` 32 | 33 | With the above `serverless.yml`, `serverless deploy` will create a zip file 34 | containing your functions. Doing this is similar to setting the Serverless 35 | `packaging.artifact` option - `cljs-lambda` is responsible for the zip contents, 36 | and Serverless includes/excludes will be skipped (`cljs-lambda` offers 37 | equivalent functionality). 38 | 39 | In the example above, there needn't be a corresponding entry for `echo` in 40 | `project.clj`. 41 | 42 | ## Lumo 43 | 44 | Alternatively you can use the [Lumo](https://github.com/anmonteiro/lumo) 45 | [compiler](https://anmonteiro.com/2017/02/compiling-clojurescript-projects-without-the-jvm/). 46 | 47 | In order to enable it, pass the `--lumo` switch to either `deploy` or `package`: 48 | 49 | ```shell 50 | $ serverless deploy --lumo 51 | ``` 52 | 53 | Or add the following to your `serverless.yml`: 54 | 55 | ```yaml 56 | custom: 57 | cljsCompiler: lumo 58 | ``` 59 | 60 | - _Compiler options_ 61 | 62 | The source paths and compiler options will be read from the optional file 63 | `serverless-lumo.edn`. Below are the defaults: 64 | 65 | ```clojure 66 | {:source-paths ["src"] 67 | :compiler {:output-to "out/lambda.js" 68 | :output-dir "out" 69 | :source-map false ;; because of a bug in lumo <= 1.8.0 70 | :target :nodejs 71 | :optimizations :none}} 72 | ``` 73 | 74 | - _Lumo Configuration_ 75 | 76 | As an alternative to `cljsCompiler: lumo`, `cljsCompiler.lumo` may be specified 77 | as a map of options. These options are passed directly to the `lumo` process. 78 | Currently supported: 79 | 80 | ```yaml 81 | custom: 82 | cljsCompiler: 83 | lumo: 84 | dependencies: 85 | - andare:0.7.0 86 | classpath: 87 | - /tmp/ 88 | localRepo: /xyz 89 | cache: /cache | none 90 | index: true | false 91 | exitOnWarning: true | false 92 | ``` 93 | 94 | _Note_: caching is always on unless you specify "none" in the config. 95 | 96 | - _The index.js file_ 97 | 98 | The `index` option will materialize a custom `index.js` in `:output-dir`'s parent folder. This 99 | file should be thought as managed by `serverless-cljs-plugin` and it is necessary for some plugin (e.g.: [`serverless-offline`](https://github.com/dherault/serverless-offline)) to work properly. 100 | 101 | _Note_: with the default compiler options, `index.js` will be saved in the project root, overwriting without warning. 102 | 103 | - _Exit on compilation warnings_ 104 | 105 | Lumo generates warnings such as `WARNING: Use of undeclared Var` to signal 106 | failures. You can tune the ones you want to see by using the 107 | [`:warnings` compiler option](https://clojurescript.org/reference/compiler-options#warnings) 108 | in `serverless-lumo.edn`, but by default the `lumo` process emits the warnings, 109 | does not throw and returns `0`. This means that `serverless` will keep going in 110 | presence of warnings. 111 | 112 | ## License 113 | 114 | serverless-cljs-plugin is free and unencumbered public domain software. For more 115 | information, see http://unlicense.org/ or the accompanying LICENSE 116 | file. 117 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const bluebird = require('bluebird'); 4 | const _ = require('lodash'); 5 | const path = require('path'); 6 | const jszip = require('jszip'); 7 | const fs = require('fs'); 8 | const minimatch = require('minimatch'); 9 | 10 | const munge = require('./serverless-cljs-plugin/munge'); 11 | const mkdirp = bluebird.promisify(require('mkdirp')); 12 | const exec = bluebird.promisify(require('child_process').exec, 13 | {multiArgs: true}); 14 | 15 | jszip.external.Promise = bluebird; 16 | 17 | const DEFAULT_EXCLUDE = ['node_modules/serverless-cljs-plugin/**', '.lumo_cache/**']; 18 | 19 | function isLumo(serverless, opts) { 20 | const compiler = _.get(serverless.service, 'custom.cljsCompiler'); 21 | const lumo = _.get(compiler, 'lumo', {}); 22 | return (compiler == "lumo" || opts.lumo || _.some(lumo)); 23 | } 24 | 25 | function setCljsLambdaFnMap(serverless, opts) { 26 | return _.mapValues( 27 | serverless.service.functions, 28 | fn => { 29 | if(fn.cljs) { 30 | fn.handler = `index.${munge.munge(fn.cljs)}`; 31 | if(!isLumo(serverless, opts)) { 32 | fn.artifact = serverless.service.__cljsArtifact; 33 | } 34 | } 35 | return fn; 36 | }); 37 | } 38 | 39 | function setCljsLambdaExclude(serverless) { 40 | return _.update(serverless.service, 'package.exclude', v => _.concat(v || [], DEFAULT_EXCLUDE)); 41 | } 42 | 43 | function edn(v) { 44 | if (_.isArray(v)) { 45 | return '[' + v.map(edn).join(' ') + ']'; 46 | } 47 | if (_.isPlainObject(v)) { 48 | return '{' + _.map(v, (v, k) => ':' + k + ' ' + edn(v)).join(' ') + '}'; 49 | } 50 | return v; 51 | } 52 | 53 | function slsToCljsLambda(functions, opts) { 54 | return _(functions) 55 | .pickBy((v, k) => v.cljs && (opts.function ? k === opts.function : true)) 56 | .values() 57 | .map (m => { 58 | return {name: `"${m.name}"`, invoke: m.cljs}; 59 | }) 60 | .thru (edn) 61 | .value (); 62 | } 63 | 64 | function basepath(config, service, opts) { 65 | return `${config.servicePath}/.serverless/${opts.function || service.service}`; 66 | } 67 | 68 | const readFile = bluebird.promisify(fs.readFile); 69 | const writeFile = bluebird.promisify(fs.writeFile); 70 | 71 | const applyZipExclude = bluebird.coroutine( 72 | function*(serverless, opts) { 73 | // TODO respect exclude/include on individual functions 74 | const exclude = _.uniq(_.get(serverless.service.package, "exclude", [])); 75 | 76 | if (!_.isEmpty(exclude)) { 77 | const data = yield readFile(serverless.service.__cljsArtifact); 78 | const oldZip = yield jszip.loadAsync(data); 79 | const newZip = jszip(); 80 | 81 | oldZip 82 | .filter((path, file) => !_.some(exclude, pattern => minimatch(path, pattern))) 83 | .forEach(entry => newZip.file(entry.name, 84 | entry.nodeStream("nodebuffer"), 85 | _.pick(entry, ["unixPermissions", "dosPermissions", "comment", "date"]))); 86 | 87 | const buffer = yield newZip.generateAsync({ 88 | type: "nodebuffer", 89 | platform: process.platform, 90 | compression: "DEFLATE", 91 | comment: `Generated by serverless-cljs-plugin on ${new Date().toISOString()}`}); 92 | 93 | yield writeFile(serverless.service.__cljsArtifact, buffer); 94 | } 95 | }); 96 | 97 | function lumoClasspath(lumo) { 98 | let cp = path.resolve(__dirname, 'serverless-cljs-plugin'); 99 | if(lumo.classpath) { 100 | cp = _.isString(lumo.classpath) ? `${cp}:${lumo.classpath}` : `${cp}:${lumo.classpath.join(':')}`; 101 | } 102 | return `--classpath ${cp}`; 103 | } 104 | 105 | function lumoDependencies(lumo) { 106 | if(lumo.dependencies) { 107 | const d = _.isString(lumo.dependencies) ? lumo.dependencies : lumo.dependencies.join(','); 108 | return `--dependencies ${d}`; 109 | } 110 | } 111 | 112 | function lumoLocalRepo(lumo) { 113 | if(lumo.localRepo) { 114 | return `--local-repo ${lumo.localRepo}`; 115 | } 116 | } 117 | 118 | function lumoCache(lumo) { 119 | if(!lumo.cache) { 120 | return "--auto-cache"; 121 | } else { 122 | if(lumo.cache != "none") { 123 | return `--cache ${lumo.cache}`; 124 | } 125 | } 126 | } 127 | 128 | function cljsLambdaBuild(serverless, opts) { 129 | const fns = slsToCljsLambda(serverless.service.functions, opts); 130 | const compiler = _.get(serverless.service, 'custom.cljsCompiler'); 131 | const lumo = _.get(compiler, 'lumo', {}); 132 | const exitOnWg = _.get(lumo, 'exitOnWarning'); 133 | const index = _.get(lumo, 'index'); 134 | 135 | let cmd; 136 | if(isLumo(serverless, opts)) { 137 | const args = _.filter([lumoClasspath(lumo), 138 | lumoDependencies(lumo), 139 | lumoLocalRepo(lumo), 140 | lumoCache(lumo)]); 141 | cmd = (`lumo ${args.join(' ')} ` + 142 | `--main serverless-lumo.build ` + 143 | `--service-path ${serverless.config.servicePath} ` + 144 | `--functions '${fns}' ` + 145 | `--index ${_.defaultTo(opts.index || index, false)} ` + 146 | `--warning-exit ${_.defaultTo(opts.exitOnWarning || exitOnWg, false)}`); 147 | } else { 148 | cmd = (`lein update-in :cljs-lambda assoc :functions '${fns}' ` + 149 | `-- cljs-lambda build :output ${serverless.service.__cljsArtifact} ` + 150 | `:quiet`); 151 | } 152 | 153 | serverless.cli.log(`Executing "${cmd}"`); 154 | return exec(cmd); 155 | } 156 | 157 | const after_createDeploymentArtifacts = bluebird.coroutine( 158 | function*(serverless, opts) { 159 | yield mkdirp(`${serverless.config.servicePath}/.serverless`); 160 | 161 | yield cljsLambdaBuild(serverless, opts); 162 | 163 | if(!isLumo(serverless, opts)) { 164 | yield applyZipExclude(serverless, opts); 165 | } 166 | 167 | serverless.cli.log(`Returning artifact path ${serverless.service.__cljsArtifact}`); 168 | return serverless.service.__cljsArtifact; 169 | }); 170 | 171 | class ServerlessPlugin { 172 | constructor(serverless, opts) { 173 | 174 | opts.function = (opts.f || opts.function); 175 | 176 | serverless.service.__cljsBasePath = ( 177 | `${basepath(serverless.config, serverless.service, opts)}`); 178 | serverless.service.__cljsArtifact= `${serverless.service.__cljsBasePath}.zip`; 179 | 180 | serverless.cli.log(`Targeting ${serverless.service.__cljsArtifact}`); 181 | 182 | setCljsLambdaFnMap(serverless, opts); 183 | setCljsLambdaExclude(serverless); 184 | 185 | const buildAndMerge = after_createDeploymentArtifacts.bind( 186 | null, serverless, opts); 187 | 188 | const when = isLumo(serverless, opts) ? 'before' : 'after'; 189 | 190 | // Using the same hooks as in graphcool/serverless-plugin-typescript: 191 | // https://github.com/graphcool/serverless-plugin-typescript/blob/master/src/index.ts#L39 192 | this.hooks = { 193 | [`${when}:package:createDeploymentArtifacts`]: buildAndMerge, 194 | [`${when}:deploy:function:packageFunction`]: buildAndMerge 195 | }; 196 | } 197 | } 198 | 199 | module.exports = ServerlessPlugin; 200 | -------------------------------------------------------------------------------- /lumo-example/README.md: -------------------------------------------------------------------------------- 1 | # lumo-example 2 | 3 | Assuming a local installation of `lumo`, run `serverless package` or `serverless deploy`. 4 | -------------------------------------------------------------------------------- /lumo-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "serverless-cljs-plugin": "0.2.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lumo-example/serverless.yml: -------------------------------------------------------------------------------- 1 | service: lumo-example 2 | 3 | custom: 4 | cljsCompiler: lumo 5 | 6 | provider: 7 | name: aws 8 | runtime: nodejs4.3 9 | 10 | functions: 11 | example: 12 | cljs: lumo-example.core/example 13 | 14 | package: 15 | exclude: 16 | - node_modules/.yarn* 17 | 18 | plugins: 19 | - serverless-cljs-plugin 20 | -------------------------------------------------------------------------------- /lumo-example/src/lumo_example/core.cljs: -------------------------------------------------------------------------------- 1 | (ns lumo-example.core) 2 | 3 | (defn ^:export example [event ctx cb] 4 | (js/console.log "Called with" event ctx cb) 5 | (cb nil event)) 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-cljs-plugin", 3 | "description": "A Serverless plugin for deploying Clojurescript functions", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/nervous-systems/serverless-cljs-plugin.git" 7 | }, 8 | "author": "Moe Aboulkheir", 9 | "license": "Unlicense", 10 | "version": "0.2.2", 11 | "engines": { 12 | "node": ">=4.0" 13 | }, 14 | "files": [ 15 | "LICENSE", 16 | "README.md", 17 | "package.json", 18 | "serverless-cljs-plugin", 19 | "index.js" 20 | ], 21 | "keywords": [ 22 | "serverless", 23 | "plugin", 24 | "clojure", 25 | "clojurescript", 26 | "aws", 27 | "lambda", 28 | "amazon", 29 | "jvm", 30 | "api gateway" 31 | ], 32 | "dependencies": { 33 | "bluebird": "3.4.6", 34 | "jszip": "3.1.4", 35 | "lodash": "4.17.2", 36 | "minimatch": "3.0.4", 37 | "mkdirp": "0.5.1", 38 | "mustache": "2.3.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /serverless-cljs-plugin/munge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const specials = { 4 | ':': '_COLON_', 5 | '+': '_PLUS_', 6 | '>': '_GT_', 7 | '<': '_LT_', 8 | '=': '_EQ_', 9 | '~': '_TILDE_', 10 | '!': '_BANG_', 11 | '@': '_CIRCA_', 12 | '#': '_SHARP_', 13 | "'": '_SINGLEQUOTE_', 14 | '"': '_DOUBLEQUOTE_', 15 | '%': '_PERCENT_', 16 | '^': '_CARET_', 17 | '&': '_AMPERSAND_', 18 | '*': '_STAR_', 19 | '|': '_BAR_', 20 | '{': '_LBRACE_', 21 | '}': '_RBRACE_', 22 | '[': '_LBRACK_', 23 | ']': '_RBRACK_', 24 | '/': '_SLASH_', 25 | '\\': '_BSLASH_', 26 | '?': '_QMARK_', 27 | '-': '_', 28 | '.': '_'}; 29 | 30 | module.exports = { 31 | munge: s => Array.prototype.map.call(s, c => specials[c] || c).join('') 32 | }; 33 | -------------------------------------------------------------------------------- /serverless-cljs-plugin/serverless_lumo/build.cljs: -------------------------------------------------------------------------------- 1 | (ns serverless-lumo.build 2 | (:require fs path 3 | [clojure.string :as str] 4 | [cljs.reader :as reader] 5 | [lumo.build.api] 6 | [lumo.classpath :as classpath] 7 | [lumo.core] 8 | [lumo.io :as io] 9 | [cljs.analyzer :as ana] 10 | [serverless-lumo.index :as index])) 11 | 12 | (defn compile! 13 | "Compile ClojureScript using the lumo api." 14 | [inputs compiler-opts] 15 | (js/console.info 16 | "Invoking the Lumo compiler w/ inputs" 17 | (.stringify js/JSON (clj->js inputs))) 18 | (run! classpath/add! inputs) 19 | (lumo.build.api/build 20 | (apply lumo.build.api/inputs inputs) 21 | compiler-opts)) 22 | 23 | (defn read-conf! 24 | "Read and return the configuration map." 25 | [] 26 | (try 27 | (-> "serverless-lumo.edn" io/slurp reader/read-string) 28 | (catch js/Error e 29 | (when-not (= (.. e -code) "ENOENT") 30 | (throw e))))) 31 | 32 | (def ^{:doc "The default build config."} 33 | default-config 34 | {:source-paths ["src"] 35 | :compiler {:output-to (.format path #js {:dir "out" :base "lambda.js"}) 36 | :output-dir "out" 37 | :source-map false ;; lumo bug 38 | :target :nodejs 39 | :optimizations :none}}) 40 | 41 | (defn- output-dir [compiler] 42 | (let [s (:output-dir compiler)] 43 | (str/replace s #"/+$" ""))) 44 | 45 | (defn dump-index! 46 | [path functions compiler] 47 | (-> (index/generate-index functions compiler) 48 | (index/write-index! path))) 49 | 50 | (defn exit-on-warning! 51 | [warning-type env extra] 52 | (when (warning-type ana/*cljs-warnings*) 53 | (when-let [s (ana/error-message warning-type extra)] 54 | (binding [*print-fn* *print-err-fn*] 55 | (println "lumo error:" (ana/message env s))) 56 | (lumo.core/exit 1)))) 57 | 58 | (defn build! 59 | "Build a project." 60 | [opts cljs-lambda-opts] 61 | (let [compiler (merge (when (:warning-exit opts) 62 | {:warning-handlers [exit-on-warning!]}) 63 | (:compiler cljs-lambda-opts)) 64 | output-dir (output-dir compiler)] 65 | (dump-index! (.resolve path (:service-path opts) "index.js") 66 | (:functions opts) 67 | compiler) 68 | (compile! (:source-paths cljs-lambda-opts) compiler))) 69 | 70 | (def cli-option-map 71 | {:s :service-path 72 | :f :functions 73 | :w :warning-exit}) 74 | 75 | (defmulti parse-option (fn [k v] k)) 76 | (defmethod parse-option :default [k v] v) 77 | (defmethod parse-option :functions [k v] (reader/read-string v)) 78 | (defmethod parse-option :warning-exit [k v] (reader/read-string v)) 79 | 80 | (defn cli-options "Compute the option map" 81 | [argv] 82 | (into {} 83 | (for [[k v] (partition 2 argv) 84 | :let [k (keyword (str/replace k #"^-{1,2}" "")) 85 | k (cli-option-map k k)]] 86 | [k (parse-option k v)]))) 87 | 88 | (defn cmd-line-args [] 89 | (if-let [args cljs.core/*command-line-args*] ;; for lumo > 1.7.0 90 | args 91 | (when-let [args lumo.core/*command-line-args*] ;; for lumo <= 1.7.0 92 | args))) 93 | 94 | (defn- merge-maps [x y] 95 | (merge-with 96 | (fn [x y] 97 | (if (map? x) 98 | (merge-maps x y) 99 | y)) 100 | x y)) 101 | 102 | (defn ^:export -main [& args] 103 | (let [opts (cli-options (cmd-line-args))] 104 | (build! opts (merge-maps default-config (read-conf!))))) 105 | 106 | (set! *main-cli-fn* -main) 107 | -------------------------------------------------------------------------------- /serverless-cljs-plugin/serverless_lumo/index.cljs: -------------------------------------------------------------------------------- 1 | (ns serverless-lumo.index 2 | (:require mustache fs path process 3 | [clojure.string :as str] 4 | [serverless-lumo.templates :refer [templates]])) 5 | 6 | (defn- export-name [sym] 7 | (str/replace (name (munge sym)) #"\." "_")) 8 | 9 | (defn- fns->module-template [fns] 10 | (for [[ns fns] (group-by namespace (map :invoke fns))] 11 | {:name (munge ns) 12 | :function 13 | (for [f fns] 14 | ;; This is Clojure's munge, which isn't always going to be right 15 | {:export (export-name f) 16 | :js-name (str (munge ns) "." (munge (name f)))})})) 17 | 18 | (defn write-index! [content outpath] 19 | (.writeFileSync fs outpath content) 20 | outpath) 21 | 22 | (defn generate-index [fns compiler] 23 | (.render 24 | mustache 25 | (templates (compiler :optimizations)) 26 | (clj->js (assoc compiler :module (fns->module-template fns))))) 27 | -------------------------------------------------------------------------------- /serverless-cljs-plugin/serverless_lumo/templates.cljs: -------------------------------------------------------------------------------- 1 | (ns serverless-lumo.templates) 2 | 3 | (def template-none 4 | "require(\"./{{{output-dir}}}/goog/bootstrap/nodejs.js\"); 5 | 6 | goog.global.CLOSURE_UNCOMPILED_DEFINES = {\"cljs.core._STAR_target_STAR_\":\"nodejs\"}; 7 | 8 | require(\"./{{{output-to}}}\"); 9 | 10 | {{#module}} 11 | goog.require(\"{{name}}\"); 12 | 13 | {{#function}} 14 | exports.{{export}} = {{js-name}}; 15 | {{/function}} 16 | {{/module}} ") 17 | 18 | (def template-simple 19 | "var __CLJS_LAMBDA_NS_ROOT = require(\"./{{{output-to}}}\"); 20 | 21 | {{#module}} 22 | {{#function}} 23 | exports.{{export}} = __CLJS_LAMBDA_NS_ROOT.{{js-name}}; 24 | {{/function}} 25 | {{/module}}") 26 | 27 | (def templates 28 | {:none template-none 29 | :simple template-simple 30 | :advanced template-simple}) 31 | --------------------------------------------------------------------------------