├── .gitignore ├── CHANGELOG.md ├── LICENSE-2.0.txt ├── README.asciidoc ├── dev-resources ├── META-INF │ └── assets │ │ ├── aviso-logo.png │ │ ├── coffeescript-source.coffee │ │ ├── colors.less │ │ ├── common.jade │ │ ├── f1.txt │ │ ├── invalid-coffeescript.coffee │ │ ├── invalid-less.less │ │ ├── jade-helper.jade │ │ ├── jade-source.jade │ │ ├── jade-uris-helper.jade │ │ ├── logo.less │ │ ├── sample.less │ │ ├── stack │ │ ├── barney.js │ │ ├── bedrock.stack │ │ ├── bootstrap.js │ │ ├── bootstrap.stack │ │ ├── compiled.stack │ │ ├── fred.js │ │ ├── jquery.js │ │ ├── local.less │ │ ├── meta.stack │ │ ├── missing-component.stack │ │ ├── stack.coffee │ │ └── style.stack │ │ └── sub │ │ ├── f2.txt │ │ ├── f3.txt │ │ ├── jade-include.jade │ │ └── samedir.jade ├── example.clj ├── expected │ ├── bedrock.js │ ├── bootstrap-webjars.css │ ├── coffeescript-source.js │ ├── coffeescript-source.map │ ├── compiled-stack.js │ ├── jade-include.html │ ├── jade-source.html │ ├── jade-uris-helper.html │ ├── logo.css │ ├── meta.js │ ├── minimized │ │ ├── bootstrap.css │ │ ├── bootstrap.js │ │ ├── fred.js │ │ ├── meta.js │ │ └── sample.css │ ├── sample.css │ ├── sample.map │ └── style.css ├── logback-test.xml └── user.clj ├── java ├── com │ └── yahoo │ │ └── platform │ │ └── yui │ │ └── compressor │ │ └── CssCompressor.java └── io │ └── aviso │ └── twixt │ └── shims │ └── CompilerShim.java ├── manual ├── _template.html ├── dexy.yaml └── en │ ├── caching.ad │ ├── configuration.ad │ ├── cssmin.ad │ ├── direct-uris.ad │ ├── exceptions.ad │ ├── getting-started.ad │ ├── index.ad │ ├── jade.ad │ ├── jsmin.ad │ ├── meta.edn │ ├── notes.ad │ ├── stacks.ad │ ├── uris.ad │ └── webjars.ad ├── project.clj ├── resources └── META-INF │ ├── assets │ └── twixt │ │ ├── exception.coffee │ │ └── exception.less │ └── twixt │ ├── coffee-script.js │ └── invoke-coffeescript.js ├── spec └── io │ └── aviso │ ├── twixt_spec.clj │ └── utils_spec.clj └── src └── io └── aviso ├── twixt.clj └── twixt ├── asset.clj ├── coffee_script.clj ├── compress.clj ├── css_minification.clj ├── css_rewrite.clj ├── exceptions.clj ├── export.clj ├── fs_cache.clj ├── jade.clj ├── js_minification.clj ├── less.clj ├── memory_cache.clj ├── rhino.clj ├── ring.clj ├── schemas.clj ├── stacks.clj ├── startup.clj └── utils.clj /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | doc 3 | .lein-failures 4 | .DS_Store 5 | twixt.sublime-* 6 | pom.* 7 | .* 8 | *.iml 9 | .dexy 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.25 - 21 Mar 2017 2 | 3 | Update to Clojure 1.8.0 4 | Fix bad ns declaration in io.aviso.twixt.fs-cache 5 | 6 | ## 0.1.24 - 18 Feb 2016 7 | 8 | Redeployed on Clojars, POM problematic in previous deploy. 9 | 10 | ## 0.1.23 - 18 Feb 2016 11 | 12 | Bugfixes 13 | - Not passing the merged 'twixt-options' to 'wrap-with-exporter' in 'startup/wrap-with-twixt' 14 | - Wrong arg passed to 'ex-info' in 'js-minification/minimize-javascript-asset' 15 | - [Issue 26](https://github.com/AvisoNovate/twixt/issues/26) 16 | 17 | ## 0.1.22 - 30 Sep 2015 18 | 19 | Updated a bunch of dependencies to latest version, most important: clojure 1.7 and schema 1.0.1 . 20 | 21 | ## 0.1.21 - 21 Aug 2015 22 | 23 | Ensured that small amount of Java in the project is compiled for JDK 1.6. 24 | 25 | Removed a few reflection warnings. 26 | 27 | Upgraded various dependencies to latest. 28 | 29 | The twixt helper provided to Jade templates now has a uris() method, which accepts an array of asset path strings. 30 | 31 | ## 0.1.20 - 12 Aug 2015 32 | 33 | Added support for CSS Compression (using YUICompressor). 34 | This is normally enabled only in production mode. 35 | 36 | Revised the approach to caching significantly. 37 | File-system caching is now enabled for both production and development, but uses 38 | different sub-directories of the specified root cache folder. 39 | 40 | There have been some non-backwards compatible changes to several functions as a result. 41 | 42 | In the Twixt options map, the :cache-folder key has been renamed to :cache-dir, and moved 43 | under a new key, :cache. 44 | 45 | The new caching and locking approach helps ensure that multiple threads do no attempt 46 | to perform the same compilation steps in parallel. 47 | There is now a per-asset lock used to prevent duplicate conflicting work across 48 | multiple threads. 49 | 50 | ## 0.1.19 - 7 Aug 2015 51 | 52 | Fixed bug where compressed assets would cause a double exception (an exception, caused by 53 | an exception building the exception report). 54 | 55 | ## 0.1.18 - 31 Jul 2015 56 | 57 | Reverted Clojure compatibility to 1.6.0. 58 | 59 | ## 0.1.17 - 29 Jul 2015 60 | 61 | Updated dependencies, including Clojure to 1.7.0, and CoffeeScript compiler to 1.9.3. 62 | 63 | Clarified many APIs by introducing Prismatic Schema signatures. 64 | 65 | Added support for finding assets inside [WebJars](http://www.webjars.org/). 66 | 67 | Twixt can now export assets to specific locations (typically, under public/resources). 68 | Exports are live: monitored for changes and dynamically re-exported as needed. 69 | This is intended largely to allow Less stylesheets to live-update when using 70 | [Figwheel](https://github.com/bhauman/lein-figwheel). 71 | 72 | ## 0.1.16 - 10 Jun 2015 73 | 74 | Update dependencies to latest. 75 | 76 | ## 0.1.15 - 7 Nov 2014 77 | 78 | Added medley (0.5.3) as a dependency. 79 | 80 | ## 0.1.14 - Oct 24 2014 81 | 82 | * In-line source maps for compiled Less files (in development mode) 83 | * Update Less4J dependency to 0.8.3 84 | 85 | No closed issues 86 | 87 | ## 0.1.13 - 6 Jun 2014 88 | 89 | * Source maps for CoffeeScript 90 | * Source maps for Less (partial, pending improvements to Less4J) 91 | * Support for stacks: aggregated assets that combine into a single virtual asset 92 | * JavaScript Minification (via Google Closure) 93 | 94 | [Closed Issues](https://github.com/AvisoNovate/twixt/issues?q=milestone%3A0.1.13) 95 | 96 | ## 0.1.12 - 29 Apr 2014 97 | 98 | * Minor bug fixes; some relative paths computed incorrectly 99 | 100 | [Closed Issues](https://github.com/AvisoNovate/twixt/issues?q=milestone%3A0.1.12) 101 | 102 | ## 0.1.11 - 13 Mar 2014 103 | 104 | * Adds support for Jade helpers and variables 105 | 106 | [Closed Issues](https://github.com/AvisoNovate/twixt/issues?q=milestone%3A0.1.11) 107 | 108 | ## 0.1.10 - 7 Mar 2014 109 | 110 | * Jade `include` directive supported, with dependency tracking 111 | 112 | [Closed Issues](https://github.com/AvisoNovate/twixt/issues?q=milestone%3A0.1.10) 113 | 114 | ## 0.1.9 - 26 Feb 2014 115 | 116 | * Exception report displays map keys in sorted order 117 | * Exception report displays some system properties as lists (e.g., `java.class.path`) 118 | 119 | [Closed Issues](https://github.com/AvisoNovate/twixt/issues?q=milestone%3A0.1.9) 120 | -------------------------------------------------------------------------------- /LICENSE-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.asciidoc: -------------------------------------------------------------------------------- 1 | == Twixt - Awesome asset management for Clojure web applications 2 | 3 | image:http://clojars.org/io.aviso/twixt/latest-version.svg[Clojars Project, link="http://clojars.org/io.aviso/twixt"] 4 | 5 | image:https://drone.io/github.com/AvisoNovate/twixt/status.png[Build Status, link="https://drone.io/github.com/AvisoNovate/twixt"] 6 | 7 | Twixt is an extensible asset pipeline for use in Clojure web applications. 8 | It is designed to complement an application built using Ring and related libraries, such as Compojure. 9 | Twixt provides content transformation (such as Less to CSS), support for efficient immutable resources, 10 | and best-of-breed exception reporting. 11 | 12 | Twixt is available under the terms of the Apache Software License 2.0. 13 | 14 | link:https://portal.aviso.io/#/docs/open-source[Full Documentation] 15 | -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/aviso-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AvisoNovate/twixt/21fff9321cd760459346604263bea8f747cfb1e2/dev-resources/META-INF/assets/aviso-logo.png -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/coffeescript-source.coffee: -------------------------------------------------------------------------------- 1 | println "CoffeeScript is fun!" -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/colors.less: -------------------------------------------------------------------------------- 1 | @body-color: #898989; 2 | -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/common.jade: -------------------------------------------------------------------------------- 1 | p Content from common.jade -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/f1.txt: -------------------------------------------------------------------------------- 1 | file 1 2 | -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/invalid-coffeescript.coffee: -------------------------------------------------------------------------------- 1 | define ["dep1", "dep2"], 2 | (dep1, dep2) -> 3 | 4 | # Error is the missing comma after the string: 5 | dep1.doSomething "atrocious" 6 | argument: dep2 7 | -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/invalid-less.less: -------------------------------------------------------------------------------- 1 | body { 2 | 3 | p { 4 | color: red; 5 | 6 | // Missing close brace: -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/jade-helper.jade: -------------------------------------------------------------------------------- 1 | img(src=twixt.uri("aviso-logo.png"), title=logoTitle) -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/jade-source.jade: -------------------------------------------------------------------------------- 1 | .sidebar-placeholder 2 | h1 Sidebar Empty 3 | p.lead 4 | | The sidebar is used when selecting certain objects or operations 5 | | in the main user interface. It will open when needed, 6 | | and can be closed using the 7 | button: i.icon-chevron-left 8 | | button. 9 | -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/jade-uris-helper.jade: -------------------------------------------------------------------------------- 1 | ul: for s in twixt.uris(["stack/compiled.stack", "/colors.less"]) 2 | li= s -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/logo.less: -------------------------------------------------------------------------------- 1 | div.logo { 2 | background: data-uri("aviso-logo.png"); 3 | } -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/sample.less: -------------------------------------------------------------------------------- 1 | @import "colors.less"; 2 | 3 | body { 4 | color: @body-color; 5 | } -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/stack/barney.js: -------------------------------------------------------------------------------- 1 | var barney = "Barney Rubble"; 2 | -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/stack/bedrock.stack: -------------------------------------------------------------------------------- 1 | {:content-type "text/javascript" 2 | :components ["fred.js" "barney.js"]} -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/stack/bootstrap.stack: -------------------------------------------------------------------------------- 1 | {:content-type "text/javascript" 2 | :components ["jquery.js", "bootstrap.js"]} -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/stack/compiled.stack: -------------------------------------------------------------------------------- 1 | {:content-type "text/javascript" 2 | :components ["../coffeescript-source.coffee" "stack.coffee"]} -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/stack/fred.js: -------------------------------------------------------------------------------- 1 | var fred = "Fred Flintstone"; 2 | -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/stack/local.less: -------------------------------------------------------------------------------- 1 | div.logo { 2 | background: url("../aviso-logo.png"); 3 | } -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/stack/meta.stack: -------------------------------------------------------------------------------- 1 | {:content-type "text/javascript" 2 | :components ["bedrock.stack" "compiled.stack"]} -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/stack/missing-component.stack: -------------------------------------------------------------------------------- 1 | {:content-type "text/javascript" 2 | :components ["does-not-exist.coffee"]} -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/stack/stack.coffee: -------------------------------------------------------------------------------- 1 | showDialog = (text) -> window.alert text 2 | 3 | $("#logo").click showDialog 4 | -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/stack/style.stack: -------------------------------------------------------------------------------- 1 | {:content-type "text/css" 2 | :components ["local.less" "../sample.less"]} -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/sub/f2.txt: -------------------------------------------------------------------------------- 1 | file 2 2 | -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/sub/f3.txt: -------------------------------------------------------------------------------- 1 | file 3 2 | -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/sub/jade-include.jade: -------------------------------------------------------------------------------- 1 | include samedir.jade 2 | include ../common 3 | p Content from "sub/jade-include.jade" 4 | -------------------------------------------------------------------------------- /dev-resources/META-INF/assets/sub/samedir.jade: -------------------------------------------------------------------------------- 1 | p Content from "sub/samedir.jade" 2 | -------------------------------------------------------------------------------- /dev-resources/example.clj: -------------------------------------------------------------------------------- 1 | (ns example 2 | (:use ring.adapter.jetty 3 | io.aviso.twixt.startup) 4 | (:require [io.aviso.tracker :as t] 5 | [io.aviso.twixt :refer [get-asset-uris default-options]])) 6 | 7 | (defn handler 8 | [request] 9 | (t/track "Invoking handler (that throws exceptions)" 10 | (if (= (:uri request) "/fail") 11 | ;; This will fail at some depth: 12 | (doall 13 | (get-asset-uris (:twixt request) "invalid-coffeescript.coffee"))))) 14 | 15 | (defn app 16 | [] 17 | (wrap-with-twixt handler (assoc-in default-options [:cache :cache-dir] "target/twixt-cache") false)) 18 | 19 | (defn launch 20 | [] 21 | (run-jetty (app) {:port 8888 :join? false})) 22 | 23 | (defn shutdown 24 | [server] 25 | (.stop server)) -------------------------------------------------------------------------------- /dev-resources/expected/bedrock.js: -------------------------------------------------------------------------------- 1 | var fred = "Fred Flintstone"; 2 | var barney = "Barney Rubble"; 3 | -------------------------------------------------------------------------------- /dev-resources/expected/coffeescript-source.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.9.3 2 | (function() { 3 | println("CoffeeScript is fun!"); 4 | 5 | }).call(this); 6 | 7 | //# sourceMappingURL=coffeescript-source.coffee@source.map -------------------------------------------------------------------------------- /dev-resources/expected/coffeescript-source.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "file": "", 4 | "sourceRoot": "", 5 | "sources": [ 6 | "coffeescript-source.coffee" 7 | ], 8 | "names": [], 9 | "mappings": ";AAAA;EAAA,OAAA,CAAQ,sBAAR;AAAA", 10 | "sourcesContent": [ 11 | "println \"CoffeeScript is fun!\"" 12 | ] 13 | } -------------------------------------------------------------------------------- /dev-resources/expected/compiled-stack.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.9.3 2 | (function() { 3 | println("CoffeeScript is fun!"); 4 | 5 | }).call(this); 6 | 7 | //# sourceMappingURL=coffeescript-source.coffee@source.map 8 | // Generated by CoffeeScript 1.9.3 9 | (function() { 10 | var showDialog; 11 | 12 | showDialog = function(text) { 13 | return window.alert(text); 14 | }; 15 | 16 | $("#logo").click(showDialog); 17 | 18 | }).call(this); 19 | 20 | //# sourceMappingURL=stack.coffee@source.map 21 | -------------------------------------------------------------------------------- /dev-resources/expected/jade-include.html: -------------------------------------------------------------------------------- 1 |

Content from "sub/samedir.jade"

2 |

Content from common.jade

3 |

Content from "sub/jade-include.jade"

-------------------------------------------------------------------------------- /dev-resources/expected/jade-source.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dev-resources/expected/jade-uris-helper.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dev-resources/expected/logo.css: -------------------------------------------------------------------------------- 1 | div.logo { 2 | background: url("data:image/png;base64,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"); 3 | } 4 | 5 | /*# sourceMappingURL=logo.less@source.map */ -------------------------------------------------------------------------------- /dev-resources/expected/meta.js: -------------------------------------------------------------------------------- 1 | var fred = "Fred Flintstone"; 2 | var barney = "Barney Rubble"; 3 | // Generated by CoffeeScript 1.9.3 4 | (function() { 5 | println("CoffeeScript is fun!"); 6 | 7 | }).call(this); 8 | 9 | //# sourceMappingURL=coffeescript-source.coffee@source.map 10 | // Generated by CoffeeScript 1.9.3 11 | (function() { 12 | var showDialog; 13 | 14 | showDialog = function(text) { 15 | return window.alert(text); 16 | }; 17 | 18 | $("#logo").click(showDialog); 19 | 20 | }).call(this); 21 | 22 | //# sourceMappingURL=stack.coffee@source.map -------------------------------------------------------------------------------- /dev-resources/expected/minimized/fred.js: -------------------------------------------------------------------------------- 1 | var fred="Fred Flintstone"; 2 | -------------------------------------------------------------------------------- /dev-resources/expected/minimized/meta.js: -------------------------------------------------------------------------------- 1 | var fred="Fred Flintstone",barney="Barney Rubble";(function(){println("CoffeeScript is fun!")}).call(this);(function(){$("#logo").click(function(a){return window.alert(a)})}).call(this); 2 | -------------------------------------------------------------------------------- /dev-resources/expected/minimized/sample.css: -------------------------------------------------------------------------------- 1 | body{color:#898989} 2 | -------------------------------------------------------------------------------- /dev-resources/expected/sample.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #898989; 3 | } 4 | 5 | /*# sourceMappingURL=sample.less@source.map */ 6 | -------------------------------------------------------------------------------- /dev-resources/expected/sample.map: -------------------------------------------------------------------------------- 1 | { 2 | "version":3, 3 | "file":"sample.css", 4 | "lineCount":1, 5 | "mappings":"ACEAA;", 6 | "sources":["colors.less","sample.less"], 7 | "sourcesContent":["@body-color: #898989;\n","@import \"colors.less\";\n\nbody {\n color: @body-color;\n}"], 8 | "names":["body"] 9 | } -------------------------------------------------------------------------------- /dev-resources/expected/style.css: -------------------------------------------------------------------------------- 1 | div.logo { 2 | background: url("/assets/8ee745bf/aviso-logo.png"); 3 | } 4 | 5 | /*# sourceMappingURL=local.less@source.map */ 6 | body { 7 | color: #898989; 8 | } 9 | 10 | /*# sourceMappingURL=sample.less@source.map */ -------------------------------------------------------------------------------- /dev-resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %date{HH:mm:ss.SSS} %-5level [%thread] %logger{} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /dev-resources/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:use speclj.config 3 | clojure.pprint) 4 | (:require [schema.core :as s])) 5 | 6 | (alter-var-root #'default-config assoc :color true :reporters ["documentation"]) 7 | 8 | (s/set-fn-validation! true) -------------------------------------------------------------------------------- /java/com/yahoo/platform/yui/compressor/CssCompressor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * YUI Compressor 3 | * http://developer.yahoo.com/yui/compressor/ 4 | * Author: Julien Lecomte - http://www.julienlecomte.net/ 5 | * Author: Isaac Schlueter - http://foohack.com/ 6 | * Author: Stoyan Stefanov - http://phpied.com/ 7 | * Contributor: Dan Beam - http://danbeam.org/ 8 | * Copyright (c) 2013 Yahoo! Inc. All rights reserved. 9 | * The copyrights embodied in the content of this file are licensed 10 | * by Yahoo! Inc. under the BSD (revised) open source license. 11 | */ 12 | package com.yahoo.platform.yui.compressor; 13 | 14 | import java.io.IOException; 15 | import java.io.Reader; 16 | import java.io.Writer; 17 | import java.util.regex.Pattern; 18 | import java.util.regex.Matcher; 19 | import java.util.ArrayList; 20 | 21 | public class CssCompressor { 22 | 23 | private StringBuffer srcsb = new StringBuffer(); 24 | 25 | public CssCompressor(Reader in) throws IOException { 26 | // Read the stream... 27 | int c; 28 | while ((c = in.read()) != -1) { 29 | srcsb.append((char) c); 30 | } 31 | } 32 | 33 | /** 34 | * @param css - full css string 35 | * @param preservedToken - token to preserve 36 | * @param tokenRegex - regex to find token 37 | * @param removeWhiteSpace - remove any white space in the token 38 | * @param preservedTokens - array of token values 39 | * @return 40 | */ 41 | protected String preserveToken(String css, String preservedToken, 42 | String tokenRegex, boolean removeWhiteSpace, ArrayList preservedTokens) { 43 | 44 | int maxIndex = css.length() - 1; 45 | int appendIndex = 0; 46 | 47 | StringBuffer sb = new StringBuffer(); 48 | 49 | Pattern p = Pattern.compile(tokenRegex); 50 | Matcher m = p.matcher(css); 51 | 52 | while (m.find()) { 53 | int startIndex = m.start() + (preservedToken.length() + 1); 54 | String terminator = m.group(1); 55 | 56 | // skip this, if CSS was already copied to "sb" upto this position 57 | if (m.start() < appendIndex) { 58 | continue; 59 | } 60 | 61 | if (terminator.length() == 0) { 62 | terminator = ")"; 63 | } 64 | 65 | boolean foundTerminator = false; 66 | 67 | int endIndex = m.end() - 1; 68 | while(foundTerminator == false && endIndex+1 <= maxIndex) { 69 | endIndex = css.indexOf(terminator, endIndex+1); 70 | 71 | if (endIndex <= 0) { 72 | break; 73 | } else if ((endIndex > 0) && (css.charAt(endIndex-1) != '\\')) { 74 | foundTerminator = true; 75 | if (!")".equals(terminator)) { 76 | endIndex = css.indexOf(")", endIndex); 77 | } 78 | } 79 | } 80 | 81 | // Enough searching, start moving stuff over to the buffer 82 | sb.append(css.substring(appendIndex, m.start())); 83 | 84 | if (foundTerminator) { 85 | String token = css.substring(startIndex, endIndex); 86 | if(removeWhiteSpace) 87 | token = token.replaceAll("\\s+", ""); 88 | preservedTokens.add(token); 89 | 90 | String preserver = preservedToken + "(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___)"; 91 | sb.append(preserver); 92 | 93 | appendIndex = endIndex + 1; 94 | } else { 95 | // No end terminator found, re-add the whole match. Should we throw/warn here? 96 | sb.append(css.substring(m.start(), m.end())); 97 | appendIndex = m.end(); 98 | } 99 | } 100 | 101 | sb.append(css.substring(appendIndex)); 102 | 103 | return sb.toString(); 104 | } 105 | 106 | public void compress(Writer out, int linebreakpos) 107 | throws IOException { 108 | 109 | Pattern p; 110 | Matcher m; 111 | String css = srcsb.toString(); 112 | 113 | int startIndex = 0; 114 | int endIndex = 0; 115 | int i = 0; 116 | int max = 0; 117 | ArrayList preservedTokens = new ArrayList(0); 118 | ArrayList comments = new ArrayList(0); 119 | String token; 120 | int totallen = css.length(); 121 | String placeholder; 122 | 123 | 124 | StringBuffer sb = new StringBuffer(css); 125 | 126 | // collect all comment blocks... 127 | while ((startIndex = sb.indexOf("/*", startIndex)) >= 0) { 128 | endIndex = sb.indexOf("*/", startIndex + 2); 129 | if (endIndex < 0) { 130 | endIndex = totallen; 131 | } 132 | 133 | token = sb.substring(startIndex + 2, endIndex); 134 | comments.add(token); 135 | sb.replace(startIndex + 2, endIndex, "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.size() - 1) + "___"); 136 | startIndex += 2; 137 | } 138 | css = sb.toString(); 139 | 140 | 141 | css = this.preserveToken(css, "url", "(?i)url\\(\\s*([\"']?)data\\:", true, preservedTokens); 142 | css = this.preserveToken(css, "calc", "(?i)calc\\(\\s*([\"']?)", false, preservedTokens); 143 | css = this.preserveToken(css, "progid:DXImageTransform.Microsoft.Matrix", "(?i)progid:DXImageTransform.Microsoft.Matrix\\s*([\"']?)", false, preservedTokens); 144 | 145 | 146 | // preserve strings so their content doesn't get accidentally minified 147 | sb = new StringBuffer(); 148 | p = Pattern.compile("(\"([^\\\\\"]|\\\\.|\\\\)*\")|(\'([^\\\\\']|\\\\.|\\\\)*\')"); 149 | m = p.matcher(css); 150 | while (m.find()) { 151 | token = m.group(); 152 | char quote = token.charAt(0); 153 | token = token.substring(1, token.length() - 1); 154 | 155 | // maybe the string contains a comment-like substring? 156 | // one, maybe more? put'em back then 157 | if (token.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) { 158 | for (i = 0, max = comments.size(); i < max; i += 1) { 159 | token = token.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments.get(i).toString()); 160 | } 161 | } 162 | 163 | // minify alpha opacity in filter strings 164 | token = token.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity="); 165 | 166 | preservedTokens.add(token); 167 | String preserver = quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___" + quote; 168 | m.appendReplacement(sb, preserver); 169 | } 170 | m.appendTail(sb); 171 | css = sb.toString(); 172 | 173 | 174 | // strings are safe, now wrestle the comments 175 | for (i = 0, max = comments.size(); i < max; i += 1) { 176 | 177 | token = comments.get(i).toString(); 178 | placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___"; 179 | 180 | // ! in the first position of the comment means preserve 181 | // so push to the preserved tokens while stripping the ! 182 | if (token.startsWith("!")) { 183 | preservedTokens.add(token); 184 | css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); 185 | continue; 186 | } 187 | 188 | // \ in the last position looks like hack for Mac/IE5 189 | // shorten that to /*\*/ and the next one to /**/ 190 | if (token.endsWith("\\")) { 191 | preservedTokens.add("\\"); 192 | css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); 193 | i = i + 1; // attn: advancing the loop 194 | preservedTokens.add(""); 195 | css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); 196 | continue; 197 | } 198 | 199 | // keep empty comments after child selectors (IE7 hack) 200 | // e.g. html >/**/ body 201 | if (token.length() == 0) { 202 | startIndex = css.indexOf(placeholder); 203 | if (startIndex > 2) { 204 | if (css.charAt(startIndex - 3) == '>') { 205 | preservedTokens.add(""); 206 | css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); 207 | } 208 | } 209 | } 210 | 211 | // in all other cases kill the comment 212 | css = css.replace("/*" + placeholder + "*/", ""); 213 | } 214 | 215 | // preserve \9 IE hack 216 | final String backslash9 = "\\9"; 217 | while (css.indexOf(backslash9) > -1) { 218 | preservedTokens.add(backslash9); 219 | css = css.replace(backslash9, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); 220 | } 221 | 222 | // Normalize all whitespace strings to single spaces. Easier to work with that way. 223 | css = css.replaceAll("\\s+", " "); 224 | 225 | // Remove the spaces before the things that should not have spaces before them. 226 | // But, be careful not to turn "p :link {...}" into "p:link{...}" 227 | // Swap out any pseudo-class colons with the token, and then swap back. 228 | sb = new StringBuffer(); 229 | p = Pattern.compile("(^|\\})(([^\\{:])+:)+([^\\{]*\\{)"); 230 | m = p.matcher(css); 231 | while (m.find()) { 232 | String s = m.group(); 233 | s = s.replaceAll(":", "___YUICSSMIN_PSEUDOCLASSCOLON___"); 234 | s = s.replaceAll( "\\\\", "\\\\\\\\" ).replaceAll( "\\$", "\\\\\\$" ); 235 | m.appendReplacement(sb, s); 236 | } 237 | m.appendTail(sb); 238 | css = sb.toString(); 239 | // Remove spaces before the things that should not have spaces before them. 240 | css = css.replaceAll("\\s+([!{};:>+\\(\\)\\],])", "$1"); 241 | // Restore spaces for !important 242 | css = css.replaceAll("!important", " !important"); 243 | // bring back the colon 244 | css = css.replaceAll("___YUICSSMIN_PSEUDOCLASSCOLON___", ":"); 245 | 246 | // retain space for special IE6 cases 247 | sb = new StringBuffer(); 248 | p = Pattern.compile("(?i):first\\-(line|letter)(\\{|,)"); 249 | m = p.matcher(css); 250 | while (m.find()) { 251 | m.appendReplacement(sb, ":first-" + m.group(1).toLowerCase() + " " + m.group(2)); 252 | } 253 | m.appendTail(sb); 254 | css = sb.toString(); 255 | 256 | // no space after the end of a preserved comment 257 | css = css.replaceAll("\\*/ ", "*/"); 258 | 259 | // If there are multiple @charset directives, push them to the top of the file. 260 | sb = new StringBuffer(); 261 | p = Pattern.compile("(?i)^(.*)(@charset)( \"[^\"]*\";)"); 262 | m = p.matcher(css); 263 | while (m.find()) { 264 | String s = m.group(1).replaceAll("\\\\", "\\\\\\\\").replaceAll("\\$", "\\\\\\$"); 265 | m.appendReplacement(sb, m.group(2).toLowerCase() + m.group(3) + s); 266 | } 267 | m.appendTail(sb); 268 | css = sb.toString(); 269 | 270 | // When all @charset are at the top, remove the second and after (as they are completely ignored). 271 | sb = new StringBuffer(); 272 | p = Pattern.compile("(?i)^((\\s*)(@charset)( [^;]+;\\s*))+"); 273 | m = p.matcher(css); 274 | while (m.find()) { 275 | m.appendReplacement(sb, m.group(2) + m.group(3).toLowerCase() + m.group(4)); 276 | } 277 | m.appendTail(sb); 278 | css = sb.toString(); 279 | 280 | // lowercase some popular @directives (@charset is done right above) 281 | sb = new StringBuffer(); 282 | p = Pattern.compile("(?i)@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)"); 283 | m = p.matcher(css); 284 | while (m.find()) { 285 | m.appendReplacement(sb, '@' + m.group(1).toLowerCase()); 286 | } 287 | m.appendTail(sb); 288 | css = sb.toString(); 289 | 290 | // lowercase some more common pseudo-elements 291 | sb = new StringBuffer(); 292 | p = Pattern.compile("(?i):(active|after|before|checked|disabled|empty|enabled|first-(?:child|of-type)|focus|hover|last-(?:child|of-type)|link|only-(?:child|of-type)|root|:selection|target|visited)"); 293 | m = p.matcher(css); 294 | while (m.find()) { 295 | m.appendReplacement(sb, ':' + m.group(1).toLowerCase()); 296 | } 297 | m.appendTail(sb); 298 | css = sb.toString(); 299 | 300 | // lowercase some more common functions 301 | sb = new StringBuffer(); 302 | p = Pattern.compile("(?i):(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:moz|webkit)-)?any)\\("); 303 | m = p.matcher(css); 304 | while (m.find()) { 305 | m.appendReplacement(sb, ':' + m.group(1).toLowerCase() + '('); 306 | } 307 | m.appendTail(sb); 308 | css = sb.toString(); 309 | 310 | // lower case some common function that can be values 311 | // NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us right after this 312 | sb = new StringBuffer(); 313 | p = Pattern.compile("(?i)([:,\\( ]\\s*)(attr|color-stop|from|rgba|to|url|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|max|min|(?:repeating-)?(?:linear|radial)-gradient)|-webkit-gradient)"); 314 | m = p.matcher(css); 315 | while (m.find()) { 316 | m.appendReplacement(sb, m.group(1) + m.group(2).toLowerCase()); 317 | } 318 | m.appendTail(sb); 319 | css = sb.toString(); 320 | 321 | // Put the space back in some cases, to support stuff like 322 | // @media screen and (-webkit-min-device-pixel-ratio:0){ 323 | css = css.replaceAll("(?i)\\band\\(", "and ("); 324 | 325 | // Remove the spaces after the things that should not have spaces after them. 326 | css = css.replaceAll("([!{}:;>+\\(\\[,])\\s+", "$1"); 327 | 328 | // remove unnecessary semicolons 329 | css = css.replaceAll(";+}", "}"); 330 | 331 | // Replace 0(px,em,%) with 0. 332 | css = css.replaceAll("(?i)(^|[^.0-9])(?:0?\\.)?0(?:px|em|%|in|cm|mm|pc|pt|ex|deg|g?rad|m?s|k?hz)", "$10"); 333 | // Replace x.0(px,em,%) with x(px,em,%). 334 | css = css.replaceAll("([0-9])\\.0(px|em|%|in|cm|mm|pc|pt|ex|deg|g?rad|m?s|k?hz| |;)", "$1$2"); 335 | 336 | // Replace 0 0 0 0; with 0. 337 | css = css.replaceAll(":0 0 0 0(;|})", ":0$1"); 338 | css = css.replaceAll(":0 0 0(;|})", ":0$1"); 339 | css = css.replaceAll(":0 0(;|})", ":0$1"); 340 | 341 | 342 | // Replace background-position:0; with background-position:0 0; 343 | // same for transform-origin 344 | sb = new StringBuffer(); 345 | p = Pattern.compile("(?i)(background-position|webkit-mask-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|})"); 346 | m = p.matcher(css); 347 | while (m.find()) { 348 | m.appendReplacement(sb, m.group(1).toLowerCase() + ":0 0" + m.group(2)); 349 | } 350 | m.appendTail(sb); 351 | css = sb.toString(); 352 | 353 | // Replace 0.6 to .6, but only when preceded by : or a white-space 354 | css = css.replaceAll("(:|\\s)0+\\.(\\d+)", "$1.$2"); 355 | 356 | // Shorten colors from rgb(51,102,153) to #336699 357 | // This makes it more likely that it'll get further compressed in the next step. 358 | p = Pattern.compile("rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\)"); 359 | m = p.matcher(css); 360 | sb = new StringBuffer(); 361 | while (m.find()) { 362 | String[] rgbcolors = m.group(1).split(","); 363 | StringBuffer hexcolor = new StringBuffer("#"); 364 | for (i = 0; i < rgbcolors.length; i++) { 365 | int val = Integer.parseInt(rgbcolors[i]); 366 | if (val < 16) { 367 | hexcolor.append("0"); 368 | } 369 | 370 | // If someone passes an RGB value that's too big to express in two characters, round down. 371 | // Probably should throw out a warning here, but generating valid CSS is a bigger concern. 372 | if (val > 255) { 373 | val = 255; 374 | } 375 | hexcolor.append(Integer.toHexString(val)); 376 | } 377 | m.appendReplacement(sb, hexcolor.toString()); 378 | } 379 | m.appendTail(sb); 380 | css = sb.toString(); 381 | 382 | // Shorten colors from #AABBCC to #ABC. Note that we want to make sure 383 | // the color is not preceded by either ", " or =. Indeed, the property 384 | // filter: chroma(color="#FFFFFF"); 385 | // would become 386 | // filter: chroma(color="#FFF"); 387 | // which makes the filter break in IE. 388 | // We also want to make sure we're only compressing #AABBCC patterns inside { }, not id selectors ( #FAABAC {} ) 389 | // We also want to avoid compressing invalid values (e.g. #AABBCCD to #ABCD) 390 | p = Pattern.compile("(\\=\\s*?[\"']?)?" + "#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])" + "(:?\\}|[^0-9a-fA-F{][^{]*?\\})"); 391 | 392 | m = p.matcher(css); 393 | sb = new StringBuffer(); 394 | int index = 0; 395 | 396 | while (m.find(index)) { 397 | 398 | sb.append(css.substring(index, m.start())); 399 | 400 | boolean isFilter = (m.group(1) != null && !"".equals(m.group(1))); 401 | 402 | if (isFilter) { 403 | // Restore, as is. Compression will break filters 404 | sb.append(m.group(1) + "#" + m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7)); 405 | } else { 406 | if( m.group(2).equalsIgnoreCase(m.group(3)) && 407 | m.group(4).equalsIgnoreCase(m.group(5)) && 408 | m.group(6).equalsIgnoreCase(m.group(7))) { 409 | 410 | // #AABBCC pattern 411 | sb.append("#" + (m.group(3) + m.group(5) + m.group(7)).toLowerCase()); 412 | 413 | } else { 414 | 415 | // Non-compressible color, restore, but lower case. 416 | sb.append("#" + (m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7)).toLowerCase()); 417 | } 418 | } 419 | 420 | index = m.end(7); 421 | } 422 | 423 | sb.append(css.substring(index)); 424 | css = sb.toString(); 425 | 426 | // Replace #f00 -> red 427 | css = css.replaceAll("(:|\\s)(#f00)(;|})", "$1red$3"); 428 | // Replace other short color keywords 429 | css = css.replaceAll("(:|\\s)(#000080)(;|})", "$1navy$3"); 430 | css = css.replaceAll("(:|\\s)(#808080)(;|})", "$1gray$3"); 431 | css = css.replaceAll("(:|\\s)(#808000)(;|})", "$1olive$3"); 432 | css = css.replaceAll("(:|\\s)(#800080)(;|})", "$1purple$3"); 433 | css = css.replaceAll("(:|\\s)(#c0c0c0)(;|})", "$1silver$3"); 434 | css = css.replaceAll("(:|\\s)(#008080)(;|})", "$1teal$3"); 435 | css = css.replaceAll("(:|\\s)(#ffa500)(;|})", "$1orange$3"); 436 | css = css.replaceAll("(:|\\s)(#800000)(;|})", "$1maroon$3"); 437 | 438 | // border: none -> border:0 439 | sb = new StringBuffer(); 440 | p = Pattern.compile("(?i)(border|border-top|border-right|border-bottom|border-left|outline|background):none(;|})"); 441 | m = p.matcher(css); 442 | while (m.find()) { 443 | m.appendReplacement(sb, m.group(1).toLowerCase() + ":0" + m.group(2)); 444 | } 445 | m.appendTail(sb); 446 | css = sb.toString(); 447 | 448 | // shorter opacity IE filter 449 | css = css.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity="); 450 | 451 | // Find a fraction that is used for Opera's -o-device-pixel-ratio query 452 | // Add token to add the "\" back in later 453 | css = css.replaceAll("\\(([\\-A-Za-z]+):([0-9]+)\\/([0-9]+)\\)", "($1:$2___YUI_QUERY_FRACTION___$3)"); 454 | 455 | // Remove empty rules. 456 | css = css.replaceAll("[^\\}\\{/;]+\\{\\}", ""); 457 | 458 | // Add "\" back to fix Opera -o-device-pixel-ratio query 459 | css = css.replaceAll("___YUI_QUERY_FRACTION___", "/"); 460 | 461 | // TODO: Should this be after we re-insert tokens. These could alter the break points. However then 462 | // we'd need to make sure we don't break in the middle of a string etc. 463 | if (linebreakpos >= 0) { 464 | // Some source control tools don't like it when files containing lines longer 465 | // than, say 8000 characters, are checked in. The linebreak option is used in 466 | // that case to split long lines after a specific column. 467 | i = 0; 468 | int linestartpos = 0; 469 | sb = new StringBuffer(css); 470 | while (i < sb.length()) { 471 | char c = sb.charAt(i++); 472 | if (c == '}' && i - linestartpos > linebreakpos) { 473 | sb.insert(i, '\n'); 474 | linestartpos = i; 475 | } 476 | } 477 | 478 | css = sb.toString(); 479 | } 480 | 481 | // Replace multiple semi-colons in a row by a single one 482 | // See SF bug #1980989 483 | css = css.replaceAll(";;+", ";"); 484 | 485 | // restore preserved comments and strings 486 | for(i = 0, max = preservedTokens.size(); i < max; i++) { 487 | css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens.get(i).toString()); 488 | } 489 | 490 | // Trim the final string (for any leading or trailing white spaces) 491 | css = css.trim(); 492 | 493 | // Write the output... 494 | out.write(css); 495 | } 496 | } -------------------------------------------------------------------------------- /java/io/aviso/twixt/shims/CompilerShim.java: -------------------------------------------------------------------------------- 1 | package io.aviso.twixt.shims; 2 | 3 | import com.google.javascript.jscomp.*; 4 | import com.google.javascript.jscomp.Compiler; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.nio.charset.StandardCharsets; 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | 15 | /** 16 | * A shim for creating and executing the Google Closure JavaScript compiler. 17 | */ 18 | public class CompilerShim { 19 | 20 | public static List runCompiler(CompilerOptions options, 21 | CompilationLevel level, 22 | String filePath, 23 | InputStream source) throws IOException { 24 | Compiler compiler = new Compiler(); 25 | 26 | compiler.disableThreads(); 27 | 28 | level.setOptionsForCompilationLevel(options); 29 | 30 | SourceFile sourceFile = SourceFile.fromInputStream(filePath, source, StandardCharsets.UTF_8); 31 | 32 | List externs = Collections.emptyList(); 33 | List sources = Arrays.asList(sourceFile); 34 | 35 | Result result = compiler.compile(externs, sources, options); 36 | 37 | List resultTuple = new ArrayList(); 38 | 39 | if (result.success) { 40 | resultTuple.add(null); 41 | resultTuple.add(compiler.toSource()); 42 | } else { 43 | resultTuple.add(result.errors); 44 | } 45 | 46 | return resultTuple; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /manual/_template.html: -------------------------------------------------------------------------------- 1 | title: {{ page_title }} 2 | --- 3 | {{ content }} 4 | -------------------------------------------------------------------------------- /manual/dexy.yaml: -------------------------------------------------------------------------------- 1 | docs: 2 | - "*/*.ad|jinja|yamlargs|asciidoctor|soups": 3 | - asciidoctor: { ext : .html , args: '-s' } 4 | - apply-ws-to-content: True 5 | - code 6 | 7 | meta: 8 | - "*/meta.edn": 9 | - output: True 10 | code: 11 | - "*/*.clj" -------------------------------------------------------------------------------- /manual/en/caching.ad: -------------------------------------------------------------------------------- 1 | title: Caching 2 | --- 3 | 4 | It is desirable to have Twixt be able to serve-up files quickly, especially in production. 5 | However, that is counter-balanced by the need to ensure the *correct* content is served. 6 | 7 | The Twixt options specifies the location of the file system cache used to store compiled assets. 8 | 9 | The location of this is specified in the key `[:cache :cache-dir]` in the Twixt options. 10 | 11 | The cache-dir is the root level directory; actually caching occurs in sub-folders for "development" 12 | or "production". 13 | 14 | The cache folder is not cleared on startup; this allow compilation and other transformations 15 | from previous executions 16 | 17 | Generally, when a request for an asset is received, and the asset is in the cache, then 18 | the response is served from the cache. 19 | 20 | Periodically (as defined by the `[:cache :cache-interval-ms]` options key), a request for an asset 21 | will perform a check against the dependencies of the asset; if any of the underlying resources have changed, then the 22 | cache entry for the asset will be discarded and rebuilt. 23 | 24 | In production, you may want to set the cache interval to be much higher than in development. 25 | 26 | On concern is that a change in configuration can also invalidate caches. 27 | For example, if you upgrade to a newer version of Twixt, or make changes to the configuration 28 | of Twixt between executions, then the cached versions of the assets may not match what 29 | Twixt would generate. 30 | 31 | You should take this into account while developing and especially when deploying: it may be necessary 32 | to manually delete the file system cache as part of deploying an application upgrade. 33 | 34 | -------------------------------------------------------------------------------- /manual/en/configuration.ad: -------------------------------------------------------------------------------- 1 | title: Configuration 2 | --- 3 | 4 | Twixt's configuration is used to determine where to locate asset resources on the classpath, 5 | and what folder to serve them under. It also maps file name extensions to MIME types, and 6 | configures the file system cache. 7 | 8 | The default options: 9 | 10 | [source,clojure] 11 | ---- 12 | (def default-options 13 | {:path-prefix "/assets/" 14 | :content-types mime/default-mime-types 15 | :resolver-factories [...] 16 | :twixt-template {} 17 | :content-transformers {} 18 | :js-optimizations :default 19 | :compressable #{"text/*" "application/edn" "application/json"} 20 | :cache {:cache-dir (System/getProperty "twixt.cache-dir" (System/getProperty "java.io.tmpdir")) 21 | :check-interval-ms 1000} 22 | :exports {:interval-ms 5000 23 | :output-dir "resources/public" 24 | :output-uri "" 25 | :assets []}} 26 | ---- 27 | 28 | You can override :path-prefix to change the root URL for assets; / is an acceptable value. 29 | 30 | The :content-types key maps file extensions to MIME types. 31 | 32 | The :content-transformers key is a map of content type keys (as strings, such as "text/coffeescript") to a 33 | transformation function; The CoffeeScript, Jade, and Less compilers operate by adding entries to :content-types and :content-transformers. 34 | 35 | The :compressable key is a set used to identify which content types are compressable; note the use of the /* suffix to indicate 36 | that all text content types are compressable. Anything not explicitly compressable is considered non-compressable. 37 | 38 | The :twixt-template key is a map that provides default values for the :twixt request key. 39 | This is often used to provide information specific to particular content transformers. 40 | 41 | The :js-optimisations key is a keyword that specifies which Google Closure compilation level should be used to minimize 42 | JavaScript files. 43 | This may be one of :default, :none, :simple, :whitespace, :advanced. 44 | 45 | The :default option corresponds to :none in development mode, and :simple in production mode. 46 | 47 | The :resolver-factories key is a list of functions that create Asset resolvers. 48 | Each factory function is passed the Twixt configuration and returns an Asset handler. 49 | The two built-in factories search for standard resources (under META-INF/assets) and WebJars resources (under META-INF/resources/webjars). 50 | 51 | The :cache key defines where the file system cache (of compiled assets) is stored, and 52 | a what interval should invalidation checks occur (invalidation checks detect when any of the files from which an asset 53 | is obtained and compiled have changed). 54 | 55 | :exports is used to configure static exports. A preset list of exports are located by their asset name and output under 56 | the configured output directory. 57 | 58 | Each value in the :assets collection can be a string, or a tuple of asset name and output alias. 59 | This allows an asset to be exported and renamed at the same time. 60 | 61 | For example, to make the asset css/site.less visible as css/site.css, you would supply a tuple: 62 | 63 | [source,clojure] 64 | ---- 65 | (-> default-options 66 | (update-in [:exports :assets] conj ["css/site.less" "css/site.css"])) 67 | ---- 68 | 69 | This would locate the resource file META-INF/assets/css/site.less, compile it from Less to CSS, and copy it 70 | to resources/public/css/site.css. 71 | A call to the io.aviso.twixt/get-asset-uri function would return the URI "/css/site.css", 72 | which is exactly what the client will need to read the downloaded file. 73 | 74 | Assets are scanned for changes at intervals, and exports occur automatically. 75 | The thread which performs asset exports is started as needed, and will shutdown 76 | once the Ring handler pipeline is garbage collected; you should generally include a call to (System/gc) to 77 | ensure this happens promptly. 78 | 79 | The output-uri is the URI corresponding to the output-dir. 80 | 81 | Twixt does *not* provide a resource handler (to pick up files exported to the output directory) on its own; this is something 82 | you must supply when setting up your Ring pipeline. 83 | 84 | 85 | -------------------------------------------------------------------------------- /manual/en/cssmin.ad: -------------------------------------------------------------------------------- 1 | title: CSS Minification 2 | --- 3 | 4 | CSS minification is enabled by default in production mode, but is normally disabled in development mode. 5 | 6 | The :minimize-css key in the link:configuration.html[Twixt options] overrides the default. 7 | 8 | -------------------------------------------------------------------------------- /manual/en/direct-uris.ad: -------------------------------------------------------------------------------- 1 | title: Direct URIs 2 | --- 3 | 4 | Sometimes it is not possible to determine the full asset URI ahead of time; 5 | a common example would be a client-side 6 | framework, such as http://angularjs.org[AngularJS] that wants to load HTML templates dynamically, at runtime. It will know 7 | the path to the asset, but will not know the checksum. 8 | 9 | In this case, an *optional* Ring middleware can be used: +wrap-with-asset-redirector+. 10 | 11 | This middleware identifies requests that match existing assets and responds with a 302 redirect to the proper asset URL. 12 | For example, the asset stored as +META-INF/assets/blueant/cla.html+ can be accessed as +/blueant/cla.html+, and will be sent a redirect 13 | to +/assets/123abc/blueant/cla.html+. 14 | -------------------------------------------------------------------------------- /manual/en/exceptions.ad: -------------------------------------------------------------------------------- 1 | title: Exception Reporting 2 | --- 3 | 4 | Twixt features a very readable HTML exception report page, which displays: 5 | 6 | * The entire stack of nested exceptions, top to bottom 7 | * The stack trace for only the root exception 8 | * Demangled namespace and function names for Clojure stack frames: `io.aviso.twixt/new-twixt/reify/middleware/fn` instead of 9 | `io.aviso.twixt$new_twixt$reify__954$middleware__959$fn__960.invoke()`. 10 | * The contents of the Ring request map 11 | * All JVM system properties 12 | 13 | Twixt provides middleware that wraps the request in a `try` block; exceptions are caught and converted to the HTML exception report, which is passed down to the client. 14 | Twixt does not attempt to perform content negotiation: it always sends a `text/html` stream. 15 | -------------------------------------------------------------------------------- /manual/en/getting-started.ad: -------------------------------------------------------------------------------- 1 | title: Getting Started 2 | --- 3 | 4 | Twixt serves resources located on the classpath, in the +META-INF/assets/+ folder. 5 | The contents of this folder is accessible to clients, by default, via the URL +/assets/+. 6 | 7 | By design, assets are segregated from the rest of your code. 8 | This prevents a malicious client from directly accessing your code or configuration files. 9 | Anything outside the +META-INF/assets/+ folder is inaccessible via Twixt. 10 | 11 | Twixt maps file extensions to MIME types; it will then transform certain MIME types; for example +.coffee+ files are compiled to JavaScript. 12 | 13 | [source,clojure] 14 | ---- 15 | (ns example.app 16 | (:use compojure.core 17 | ring.adapter.jetty 18 | [io.aviso.twixt.startup :as startup])) 19 | 20 | ;;; Use Compojure to map routes to handlers 21 | (defroutes app ...) 22 | 23 | ;;; Create twixt wrappers and handler to handle /asset/ URLs; 24 | ;;; true -> in development mode 25 | (def app-handler 26 | (startup/wrap-with-twixt app true) 27 | 28 | ;;; Use the Ring Jetty adapter to serve your application 29 | (run-jetty app-handler) 30 | ---- 31 | 32 | NOTE: Twixt changes its behavior in a number of ways in development mode (as opposed to the normal 33 | production mode). 34 | For example, Less compilation will pretty-print the generated HTML markup in development 35 | mode. 36 | In production mode, the markup omits all unnecessary white space. 37 | 38 | The Twixt middleware intercepts requests for the +/assets/+ URI that map to actual files; non-matching requests, or 39 | requests for assets that do not exist, are delegated down to the wrapped handlers. 40 | 41 | In development mode, Twixt will write compiled files to the file system (you can configure where if you like). 42 | On a restart of the application, it will use those cached files if the source files have not changed. This is important, 43 | as compilation of some resources, such as CoffeeScript, can take several seconds (due to the speed, or lack thereof, of 44 | the Rhino JavaScript engine). 45 | 46 | The Twixt API includes alternate functions for constructing both the Ring middleware, and Twixt's own 47 | asset pipeline; this allows you to add new features, or exclude unwanted features. Please reference the 48 | code to see how to configure Twixt options, assemble the Twixt asset pipeline, and finally, provide the necessary 49 | Ring middleware. -------------------------------------------------------------------------------- /manual/en/index.ad: -------------------------------------------------------------------------------- 1 | title: About 2 | --- 3 | 4 | == Awesome asset management for Clojure web applications 5 | 6 | %={:type :github :user "AvisoNovate" :repo "twixt"}% 7 | 8 | image:http://clojars.org/io.aviso/twixt/latest-version.svg[Clojars Project, link="http://clojars.org/io.aviso/twixt"] 9 | 10 | image:https://drone.io/github.com/AvisoNovate/twixt/status.png[Build Status, link="https://drone.io/github.com/AvisoNovate/twixt"] 11 | 12 | Twixt is an extensible asset pipeline for use in Clojure web applications. 13 | It is designed to complement an application built using Ring and related libraries, such as Compojure. 14 | Twixt provides content transformation (such as Less to CSS), support for efficient immutable resources, 15 | and best-of-breed exception reporting. 16 | 17 | Twixt draws inspiration from http://tapestry.apache.org[Apache Tapestry] and https://github.com/edgecase/dieter[Dieter]. 18 | 19 | Twixt currently supports: 20 | 21 | * link:exceptions.html[Best-of-breed exception report] 22 | * link:http://coffeescript.org/[CoffeeScript] to JavaScript compilation (using Rhino) - including source maps 23 | * JavaScript Minimization (using https://developers.google.com/closure/compiler/[Google Closure]) 24 | * Jade to HTML compilation (using https://github.com/neuland/jade4j[jade4j]) 25 | * Less to CSS compilation (using https://github.com/SomMeri/less4j[less4j]) 26 | * link:stacks.html[JavaScript and CSS aggregation] 27 | * File system caching of compiled content 28 | * Automatic GZip compression 29 | * Automatic CSS Minification (using http://yui.github.io/yuicompressor/[YUICompressor]) 30 | * Works with link:webjars.html[WebJars] 31 | 32 | == Stability 33 | 34 | *Alpha*: Many features are not yet implemented and the code is likely to change in many ways going forward ... but still very useful! 35 | -------------------------------------------------------------------------------- /manual/en/jade.ad: -------------------------------------------------------------------------------- 1 | title: Jade 2 | --- 3 | 4 | link:http://jade-lang.com/[Jade] is a wonderful template engine ... as long as you 5 | are comfortable with significant indentation. 6 | 7 | Like many such tools, the real power comes from being able to extend Jade in various ways. 8 | 9 | == twixt helper 10 | 11 | Twixt places a helper object, +twixt+, into scope for your templates. +twixt+ supplies a single method, +uri+. 12 | You can pass the +uri+ method a relative path, or an absolute path (starting with a slash). 13 | 14 | ---- 15 | img(src=twixt.uri("logo.png")) 16 | ---- 17 | 18 | WARNING: When the path is relative, it is evaluated relative to the main Jade asset 19 | (and explicitly not relative to any +include+ -ed Jade sources). 20 | 21 | This will output a fully qualified asset URI: 22 | 23 | ---- 24 | 25 | ---- 26 | 27 | The +uris+ method accepts an array of paths; an returns an array of individual asset URIs. 28 | This is useful when the URI references a stack (which will be a single asset in production, 29 | but multiple assets in development). 30 | 31 | --- 32 | for script in twixt.uris(["js/app.stack"]) 33 | script(src=script) 34 | ---- 35 | 36 | == Defining your own helpers 37 | 38 | It is possible to define your own helper objects. 39 | 40 | Helper objects are defined inside the Twixt context under keys +:jade+ +:helpers+. 41 | This is a map of _string_ keys to creator functions. 42 | 43 | Each creator function is passed the main Jade asset, and the Twixt context. 44 | It uses this to initialize and return a helper object. 45 | A new set of helper objects is created for each individual Jade compilation. 46 | 47 | Generally, you will want to define a protocol, then use +reify+. For example, this is the implementation of the +twixt+ helper: 48 | 49 | ---- 50 | (defprotocol TwixtHelper 51 | "A Jade4J helper object that is used to allow a template to resolve asset URIs." 52 | (uri 53 | [this path] 54 | "Used to obtain the URI for a given asset, identified by its asset path. 55 | The path may be relative to the currently compiling asset, or may be absolute (with a leading slash). 56 | 57 | Throws an exception if the asset it not found.") 58 | (uris 59 | [this paths] 60 | "Used to obtain multiple URIs for any number of assets, each identified by a path. 61 | 62 | paths is a seq of asset path strings (eitehr relative to the current asset, or absolute). 63 | 64 | *Added in 0.1.21*")) 65 | 66 | (defn- create-twixt-helper 67 | [asset context] 68 | (reify TwixtHelper 69 | (uri [_ path] 70 | (twixt/get-asset-uri context (complete-path asset path))) 71 | (uris [_ paths] 72 | (->> paths 73 | (map (partial complete-path asset)) 74 | (mapcat (partial twixt/get-asset-uris context)))))) 75 | ---- 76 | 77 | NOTE: Any asset URI will cause the asset in question to be added as a dependency of the main Jade template. This means 78 | that changing the referenced asset will cause the Jade template to be re-compiled. This makes sense: changing an image 79 | file will change the URI for the image file (due to content snapshotting), which means that the Jade output should also change. 80 | 81 | Creator functions can be added to the Twixt context using Ring middleware: 82 | 83 | ---- 84 | (handler (assoc-in request [:twixt :jade :helpers "adrotation"] 85 | create-ad-rotation-helper)) 86 | ---- 87 | 88 | However, more frequently, you will just add to the Twixt options in your application's startup code: 89 | 90 | ---- 91 | (assoc-in twixt/default-options [:twixt-template :jade :helpers "adrotation"] 92 | create-ad-rotation-helper)) 93 | ---- 94 | 95 | This +:twixt-template+ key is used to create the +:twixt+ Ring request key. 96 | 97 | == Defining your own variables 98 | 99 | Variables are much the same as helpers, with two differences: 100 | 101 | * The key is +:variables+ (under +:jade+, in the Twixt context) 102 | * The value is the exact object to expose to the template 103 | 104 | You can expose Clojure functions as variables if you wish; the Jade template should use +func.invoke()+ to call the function. 105 | 106 | == Helper / Variable pitfalls 107 | 108 | The main issue with helpers and variables relates to cache invalidation. 109 | Twixt bases cache invalidation entirely on the contents of the underlying files. 110 | There is no way for Twixt to know to invalidate the cache just because the implementation 111 | of a helper has changed, even if that means different markup is being rendered. This 112 | is one of the primary reasons that link:caching.html[disk cache is disabled in production]. 113 | 114 | There is currently an ambiguity that comes into play when the referenced asset is a compressable file type (e.g., not an image 115 | file). This can cause the Jade compiler to generate a compressed URI that, for a different request and client, will not be useful. 116 | 117 | -------------------------------------------------------------------------------- /manual/en/jsmin.ad: -------------------------------------------------------------------------------- 1 | title: JavaScript Minimization 2 | --- 3 | 4 | JavaScript minimization is enabled by default only in production mode. 5 | 6 | It is set up for _simple optimizations_; this removes whitespace and may shorten variable and function names. 7 | 8 | The optimization level is configurable using the +:js-optimisations+ key as described in %={:type :link :page "configuration"}. 9 | 10 | It is recommended that you make use of JavaScript stacks; this allows the compiler to only be executed once 11 | on a larger sample of JavaScript. -------------------------------------------------------------------------------- /manual/en/meta.edn: -------------------------------------------------------------------------------- 1 | {:name "Twixt Manual" 2 | :description "Awesome asset management for Clojure web applications." 3 | :github "AvisoNovate/twixt" 4 | :api "http://howardlewisship.com/io.aviso/twixt/" 5 | :pages ["index" 6 | "getting-started" 7 | "uris" 8 | "exceptions" 9 | "jade" 10 | "stacks" 11 | "caching" 12 | "jsmin" 13 | "cssmin" 14 | "configuration" 15 | "direct-uris" 16 | "webjars" 17 | "notes"]} 18 | -------------------------------------------------------------------------------- /manual/en/notes.ad: -------------------------------------------------------------------------------- 1 | title: Notes 2 | --- 3 | 4 | == Future Plans 5 | 6 | The goal is to achieve at least parity with Apache Tapestry, plus some additional features specific to Clojure. This means: 7 | 8 | * E-Tags support 9 | * ClojureScript compilation 10 | * CSS Minification 11 | * RequireJS support and AMD modules 12 | * Break out the the Less, Jade, CoffeeScript, exception reporting support, etc. into a-la-carte modules 13 | * "Warm up" the cache at startup (in production) 14 | 15 | 16 | == A note about feedback 17 | 18 | http://tapestryjava.blogspot.com/2013/05/once-more-feedback-please.html[Feedback] is very important to me; I often find 19 | Clojure just a bit frustrating, because if there is an error in your code, it can be a bit of a challenge to track the problem 20 | backwards from the failure to the offending code. Part of this is inherent in functional programming, part of it is related to lazy evaluation, 21 | and part is the trade-off between a typed and untyped language. 22 | 23 | In any case, it is very important to me that when thing go wrong, you are provided with a detailed description of the failure. 24 | Twixt has a mechanism for tracking the operations it is attempting, to give you insight into what exactly failed if there 25 | is an error. For example, (from the test suite): 26 | 27 | ---- 28 | ERROR [ qtp2166970-29] io.aviso.twixt.coffee-script An exception has occurred: 29 | ERROR [ qtp2166970-29] io.aviso.twixt.coffee-script [ 1] - Invoking handler (that throws exceptions) 30 | ERROR [ qtp2166970-29] io.aviso.twixt.coffee-script [ 2] - Accessing asset `invalid-coffeescript.coffee' 31 | ERROR [ qtp2166970-29] io.aviso.twixt.coffee-script [ 3] - Compiling `META-INF/assets/invalid-coffeescript.coffee' to JavaScript 32 | ERROR [ qtp2166970-29] io.aviso.twixt.coffee-script META-INF/assets/invalid-coffeescript.coffee:6:1: error: unexpected INDENT 33 | argument: dep2 34 | ^^^^^^ 35 | java.lang.RuntimeException: META-INF/assets/invalid-coffeescript.coffee:6:1: error: unexpected INDENT 36 | argument: dep2 37 | ^^^^^^ 38 | .... 39 | ---- 40 | 41 | In other words, when there's a failure, Twixt can tell you the steps that led up the failure, which is 90% of solving the problem in the first place. 42 | 43 | Twixt's exception report captures all of this and presents it as readable HTML. 44 | The exception report page also does a decent job of de-mangling Java class names to Clojure namespaces and function names. 45 | 46 | == How does Twixt differ from Dieter? 47 | 48 | On the application I was building, I had a requirement to deploy as a JAR; Dieter expects all the assets to be on the filesystem; I spent some time attempting to hack the Dieter code to allow resources on the classpath as well. 49 | When that proved unsuccessful, I decided to build out something a bit more ambitious, that would support the features that have accumulated in Tapestry over the last few years. 50 | 51 | Twixt also embraces http://www.infoq.com/presentations/Clojure-Large-scale-patterns-techniques[system as transient state], meaning nothing is stored statically. 52 | 53 | Twixt will grow further apart from Dieter as the more advanced pieces are put into place. -------------------------------------------------------------------------------- /manual/en/stacks.ad: -------------------------------------------------------------------------------- 1 | title: Stacks 2 | --- 3 | 4 | In development, you often want to have many small source files downloaded individually to the browser. 5 | This is simpler to debug, and faster ... a change to one file will be a small recompile of just that file. 6 | 7 | In production, it's a different story; you want the client to make as few requests to the server as possible. 8 | 9 | This can be accomplished using _stacks_. 10 | 11 | Stacks allow you to group together related files of the same type into a single asset. Commonly, this is used 12 | to aggregate JavaScript or CSS. 13 | 14 | In development mode, you will see the individual files of the stack; 15 | in production mode, the stack is represented by a single URI which maps to the aggregated content of all the 16 | files in the stack. 17 | 18 | A stack file is written in https://github.com/edn-format/edn[EDN]. 19 | Each stack file contains a +:content-type+ key, and a +:components+ key. 20 | Stack files have a +.stack+ extension. 21 | 22 | [source,clojure] 23 | ---- 24 | {:content-type "text/css" 25 | :components "bootstrap3/bootstrap.css" 26 | "app.less" 27 | "ie-fixes.less"} 28 | ---- 29 | 30 | When using stacks, you will want a slight tweak to your page template: 31 | 32 | [source,clojure] 33 | ---- 34 | (defhtml index 35 | [{context :twixt :as request}] 36 | (html 37 | (doctype :html5 38 | [:html 39 | [:head 40 | [:title "My Clojure App"] 41 | (apply include-css (get-asset-uris context "css/app-css.stack")) 42 | ... 43 | ---- 44 | 45 | Since +get-asset-uris+ will return a collection of URIs (unlike +get-asset-uri+ which always returns just one), 46 | we must change +include-css+ to +apply include-css+. 47 | 48 | This template will work in development (+get-asset-uris+ returning several URIs) and in production (just a 49 | single URI). 50 | 51 | It is possible for stacks to include other stacks as components. 52 | 53 | Stack components are _included_ not _imported_; if a component asset is listed more than once, its content will 54 | be aggregated more than once. -------------------------------------------------------------------------------- /manual/en/uris.ad: -------------------------------------------------------------------------------- 1 | title: URIs 2 | --- 3 | 4 | At its core, Twixt is a set of Ring middleware that maps certain URI patterns to matching files on the classpath, 5 | and does some transformations along the way. 6 | 7 | Currently, the mapping is very straightforward: the path +/assets/123abc/css/style.less+ is mapped to resource 8 | +META-INF/assets/css/style.less+ which is read into memory and transformed from Less to CSS. 9 | Embedded in the middle of the URL is the content checksum for the file (+123abc+). 10 | 11 | NOTE: Placing this information directly into the URI is called _fingerprinting_. 12 | 13 | In your application, assets will change during development, or between production deployments. The URIs provided to 14 | the client agent (the web browser) is a _resource_; on the web, resources are immutable, even though 15 | in your workspace, files change all the time. 16 | The checksum in the URI is based on the actual content of the file; 17 | whenever the underlying content changes, then a new checksum, new URI, and therefore, new resource will be referenced by the URI. 18 | 19 | Twixt sets headers to indicate a far-future expiration date for the resource; 20 | the upshot of which is that, once the resource for an asset is downloaded to the client browser, the browser will not ask for it again. 21 | This is great for overall performance, since it reduces the amount of network I/O 22 | the client will encounter, especially on subsequent visits to the same site. 23 | 24 | It also reduces the load on the server in two ways: 25 | 26 | * Fewer requests will need to be sent to the server as client will use their local cache. 27 | * Content can be cached by intermediary servers. 28 | 29 | The checksum has a optional "z" prefix; this indicates a GZip compressed resource. 30 | 31 | On a request that renders markup, Twixt will detect if the client supports GZip compression. 32 | If so, then for assets where compression makes sense (such as JavaScript, and 33 | specifically excluding image formats) then Twixt will generate an alternate URI that 34 | indicates access to the compressed asset. 35 | 36 | Because of this, when referencing assets inside your templates, you must pass paths (relative to +META-INF/assets+) 37 | through Twixt to get URIs that will work in the browser: 38 | 39 | [source,clojure] 40 | ---- 41 | (defhtml index 42 | [{context :twixt :as request}] 43 | (html 44 | (doctype :html5 45 | [:html 46 | [:head 47 | [:title "My Clojure App"] 48 | (include-css (get-asset-uri context "css/style.less")) 49 | ... 50 | ---- 51 | 52 | The parameter to +defhtml+ is the Ring request map; Twixt has injected middleware that provides the Twixt context under 53 | the +:twixt+ key. 54 | 55 | +get-asset-uri+ is defined in the +io.aviso.twixt+ namespace. 56 | 57 | Twixt must do all necessary compilations and other transformations, to arrive at final content for which a checksum 58 | can be computed. 59 | Although this can slow the initial HTML render, it is also good because any exceptions, such as compilation errors, will occur immediately, 60 | rather than when the asset's content is later retrieved by the client. 61 | 62 | When a client requests an asset that exists, but supplies an incorrect checksum, 63 | Twixt will respond with a 301 redirect HTTP response, 64 | directing the client to the correct resource (with the correct checksum). 65 | This is an extremely unlikely scenario that would involve a running client in a race with a redeployed application. -------------------------------------------------------------------------------- /manual/en/webjars.ad: -------------------------------------------------------------------------------- 1 | title: WebJars 2 | --- 3 | 4 | link:http://www.webjars.org/[Web Jars] is any easy way to include any number of pre-packaged sets of resources 5 | as a JAR file. 6 | Why mess around with getting a distribution, extracting it, and possibly checking those files into your project's 7 | source code control? 8 | Just add the WebJar to the classpath (inside your project.clj): 9 | 10 | [source,clojure] 11 | ---- 12 | [org.webjars/d3js "3.5.5-1"] 13 | ---- 14 | 15 | And you can now reference the files within that WebJar: 16 | 17 | [source,clojure] 18 | ---- 19 | (find-asset-uri context "d3js/d3.js) 20 | ---- 21 | 22 | That's it ... you don't even need to use the version number in the call to find-asset-uri; the version 23 | number needed internally to build the path to the referenced file (in this example, /META-INF/resources/webjars/d3j3/3.5.5-1/d3.js) 24 | is determined automatically. 25 | 26 | The referenced file will be read as normal and processed just like any file under META-INF/assets. 27 | 28 | Twixt uses dependencies on Bootstrap (3.3.5) and jQuery (1.11.1), which are used in the link:exceptions.html[exception report page]. -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject io.aviso/twixt "0.1.25" 2 | :description "An extensible asset pipeline for Clojure web applications" 3 | :url "https://github.com/AvisoNovate/twixt" 4 | :license {:name "Apache Sofware Licencse 2.0" 5 | :url "http://www.apache.org/licenses/LICENSE-2.0.html"} 6 | :dependencies [[org.clojure/clojure "1.8.0"] 7 | [io.aviso/pretty "0.1.19"] 8 | [io.aviso/tracker "0.1.7"] 9 | [medley "0.7.0"] 10 | [ring/ring-core "1.4.0"] 11 | [com.google.javascript/closure-compiler "v20150729"] 12 | [org.mozilla/rhino "1.7.7"] 13 | [com.github.sommeri/less4j "1.14.0"] 14 | [de.neuland-bfi/jade4j "0.4.3"] 15 | [prismatic/schema "1.0.1"] 16 | [hiccup "1.0.5"] 17 | [org.webjars/bootstrap "3.3.5"] 18 | [org.webjars/webjars-locator-core "0.28"]] 19 | ;; We keep a local copy of YUICompressor's CSSCompressor; we don't want the rest of the dependency 20 | ;; since it does evil things to patch Rhino to support JavaScript compression. 21 | :java-source-paths ["java"] 22 | :javac-options ["-target" "1.6" "-source" "1.6"] 23 | :test-paths ["spec"] 24 | :plugins [[speclj "3.2.0"] 25 | [lein-shell "0.4.0"]] 26 | :shell {:commands {"scp" {:dir "doc"}}} 27 | :aliases {"deploy-doc" ["shell" 28 | "scp" "-r" "." "hlship_howardlewisship@ssh.phx.nearlyfreespeech.net:io.aviso/twixt"] 29 | "release" ["do" 30 | "clean," 31 | "spec,", 32 | "doc," 33 | "deploy-doc," 34 | "deploy" "clojars"]} 35 | :codox {:src-dir-uri "https://github.com/AvisoNovate/twixt/blob/master/" 36 | :src-linenum-anchor-prefix "L" 37 | :defaults {:doc/format :markdown}} 38 | :profiles {:dev {:dependencies [[ch.qos.logback/logback-classic "1.1.3"] 39 | [speclj "3.3.1"] 40 | [ring/ring-jetty-adapter "1.4.0"]] 41 | :jvm-opts ["-Xmx1g"]} 42 | :1.6 {:dependencies [[org.clojure/clojure "1.6.0"] 43 | [speclj "3.2.0"] 44 | [medley "0.6.0"]]}}) 45 | -------------------------------------------------------------------------------- /resources/META-INF/assets/twixt/exception.coffee: -------------------------------------------------------------------------------- 1 | # Adds a simple event handler that toggles body.hide-filtered when the toggle filter button is clicked. 2 | 3 | $ -> 4 | $("body").on "click", "[data-action=toggle-filter]", -> 5 | $("body").toggleClass "hide-filtered" -------------------------------------------------------------------------------- /resources/META-INF/assets/twixt/exception.less: -------------------------------------------------------------------------------- 1 | // CSS used on the exception report view; expects Bootstrap CSS to be present as well. 2 | 3 | body { 4 | margin-top: 5px; 5 | } 6 | 7 | dl:not(.dl-horizontal) dd { 8 | margin-left: 25px; 9 | } 10 | 11 | td.function-name, td.source-location { 12 | text-align: right; 13 | } 14 | 15 | .package-name { 16 | font-size: x-small; 17 | } 18 | 19 | .filtered { 20 | color: rgb(136, 136, 136); 21 | } 22 | 23 | .hide-filtered .filtered { 24 | display: none; 25 | } 26 | 27 | .spacing-below { 28 | margin-bottom: 5px; 29 | } -------------------------------------------------------------------------------- /resources/META-INF/twixt/invoke-coffeescript.js: -------------------------------------------------------------------------------- 1 | // Compiles CoffeeScript source to JavaScript. 2 | // 3 | // input - string containing contents of the file 4 | // filepath - name of file, used to report errors 5 | // filename - last term in filepath 6 | // 7 | // Returns { output: , sourceMap: source-map-as-string } or { exception: } 8 | function compileCoffeeScriptSource(input, filepath, filename) { 9 | try { 10 | var result = CoffeeScript.compile(input, { 11 | header: true, 12 | filename: filepath, 13 | sourceFiles: [filename], 14 | sourceMap: true, 15 | inline: true}); 16 | 17 | return { 18 | output: result.js + "\n//# sourceMappingURL=" + filename + "@source.map\n" , 19 | sourceMap: result.v3SourceMap 20 | }; 21 | } 22 | catch (err) { 23 | return { exception: err.toString() }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /spec/io/aviso/twixt_spec.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt-spec 2 | (:use speclj.core 3 | clojure.pprint 4 | clojure.template 5 | [io.aviso.twixt :exclude [find-asset]] 6 | io.aviso.twixt.utils) 7 | (:require [clojure.java.io :as io] 8 | [ring.middleware.resource :as resource] 9 | [io.aviso.twixt 10 | [coffee-script :as cs] 11 | [jade :as jade] 12 | [less :as less] 13 | [ring :as ring] 14 | [startup :as startup] 15 | [stacks :as stacks]] 16 | [clojure.string :as str] 17 | [clojure.tools.logging :as l] 18 | [io.aviso.twixt :as twixt])) 19 | 20 | (defn read-as-trimmed-string 21 | [content] 22 | (-> content 23 | read-content 24 | String. 25 | .trim)) 26 | 27 | (defn read-asset-content 28 | [asset] 29 | (assert asset "Can't read content from nil asset.") 30 | (-> 31 | asset 32 | :content 33 | read-as-trimmed-string)) 34 | 35 | (defn read-attachment-content 36 | [asset attachment-name] 37 | (assert asset "Can't read content from nil asset.") 38 | (-> 39 | asset 40 | (get-in [:attachments attachment-name]) 41 | :content 42 | read-as-trimmed-string)) 43 | 44 | (defn read-resource-content 45 | [path] 46 | (-> 47 | path 48 | io/resource 49 | read-as-trimmed-string)) 50 | 51 | (defmacro should-have-content 52 | [expected actual] 53 | `(let [expected# ~expected 54 | actual# ~actual] 55 | (when (not (= expected# actual#)) 56 | (-fail (format "Expected content did not match actual content:%n----%n%s%n----" 57 | actual#))))) 58 | 59 | (defn have-same-content 60 | [expected-content-resource asset] 61 | (let [expected (read-resource-content expected-content-resource) 62 | actual (read-asset-content asset)] 63 | (if (= expected actual) 64 | true 65 | (printf "Content of `%s' did not match `%s'\n----\n%s\n----\n" 66 | (:resource-path asset) 67 | expected-content-resource 68 | actual)))) 69 | 70 | (defn- sorted-dependencies 71 | [asset] 72 | (->> asset :dependencies vals (map :asset-path) sort)) 73 | 74 | (defn- remove-hash-from-uri 75 | [^String asset-uri] 76 | (str/join "/" (-> 77 | asset-uri 78 | (.split "/") 79 | vec 80 | (assoc 2 "[hash]")))) 81 | 82 | ;; Note: updating the CoffeeScript compiler will often change the outputs, including checksums, not least because 83 | ;; the compiler injects a comment with the compiler version. 84 | 85 | (def compiled-coffeescript-checksum "54753589") 86 | 87 | (describe "io.aviso.twixt" 88 | 89 | (with-all cache-dir (format "%s/%x" (System/getProperty "java.io.tmpdir") (System/nanoTime))) 90 | 91 | (defn with-sub-cache-dir [twixt-options subdir] 92 | (update-in twixt-options [:cache :cache-dir] str "/" subdir)) 93 | 94 | (defn get-asset-with-options [twixt-options asset-path] 95 | (let [pipeline (default-asset-pipeline twixt-options)] 96 | (pipeline asset-path (assoc twixt-options :asset-pipeline pipeline)))) 97 | 98 | (with-all options (-> 99 | default-options 100 | (assoc :development-mode true 101 | :js-optimizations :none) 102 | (assoc-in [:cache :cache-dir] @cache-dir) 103 | ;; This is usually done by the startup namespace: 104 | cs/register-coffee-script 105 | jade/register-jade 106 | less/register-less 107 | stacks/register-stacks)) 108 | 109 | (with-all pipeline (default-asset-pipeline @options)) 110 | 111 | (with-all twixt-context (assoc @options :asset-pipeline @pipeline)) 112 | 113 | (defn find-asset 114 | [asset-path] 115 | (@pipeline asset-path @twixt-context)) 116 | 117 | (context "asset pipeline" 118 | 119 | (it "returns nil when an asset is not found" 120 | 121 | (should-be-nil (@pipeline "does/not/exist.gif" @options)))) 122 | 123 | (context "get-asset-uri" 124 | 125 | (it "throws IllegalArgumentException if an asset does not exist" 126 | (should-throw Exception 127 | "Asset path `does/not/exist.png' does not map to an available resource." 128 | (get-asset-uri @twixt-context "does/not/exist.png"))) 129 | 130 | (it "returns the correct asset URI" 131 | (should= (str "/assets/" compiled-coffeescript-checksum "/coffeescript-source.coffee") 132 | (find-asset-uri @twixt-context "coffeescript-source.coffee"))) 133 | 134 | 135 | (it "can find WebJars assets" 136 | (should 137 | (have-same-content "META-INF/resources/webjars/bootstrap/3.3.5/js/alert.js" 138 | (find-asset "bootstrap/js/alert.js")))) 139 | 140 | (it "can process compilation of WebJars assets" 141 | (should 142 | (have-same-content "expected/bootstrap-webjars.css" 143 | (find-asset "bootstrap/less/bootstrap.less"))))) 144 | 145 | (context "CoffeeScript compilation" 146 | 147 | (with-all asset (@pipeline "coffeescript-source.coffee" @twixt-context)) 148 | 149 | (it "can read and transform a source file" 150 | 151 | (should-not-be-nil @asset) 152 | (should-not-be-nil (:modified-at @asset)) 153 | 154 | (do-template [key expected] 155 | (should= expected (key @asset) 156 | {:resource-path "META-INF/assets/coffeescript-source.coffee" 157 | :asset-path "coffeescript-source.coffee" 158 | :content-type "text/javascript" 159 | :compiled true 160 | :size 160 161 | :checksum compiled-coffeescript-checksum}))) 162 | (it "has the correct compiled content" 163 | (should (have-same-content "expected/coffeescript-source.js" @asset))) 164 | 165 | (it "has the correct source.map attachment" 166 | (should-have-content 167 | (read-resource-content "expected/coffeescript-source.map") 168 | (read-attachment-content @asset "source.map"))) 169 | 170 | (it "has the expected dependencies" 171 | (should= (sorted-dependencies @asset) 172 | ["coffeescript-source.coffee"])) 173 | 174 | (it "throws an exception if the source is not valid" 175 | (try 176 | (@pipeline "invalid-coffeescript.coffee" @twixt-context) 177 | (should-fail) 178 | (catch Exception e 179 | (should (-> e .getMessage (.startsWith "META-INF/assets/invalid-coffeescript.coffee:6:1: error: unexpected indentation"))))))) 180 | 181 | (context "Jade compilation" 182 | 183 | (context "simple Jade source" 184 | (with-all asset (@pipeline "jade-source.jade" @twixt-context)) 185 | 186 | 187 | (it "can read and transform a source file" 188 | 189 | (should= "text/html" (:content-type @asset)) 190 | (should (:compiled @asset))) 191 | 192 | (it "has the correct compiled content" 193 | (should (have-same-content "expected/jade-source.html" @asset)))) 194 | 195 | (context "using Jade includes" 196 | 197 | (with-all asset (@pipeline "sub/jade-include.jade" @twixt-context)) 198 | 199 | (it "has the correct compiled content" 200 | (should (have-same-content "expected/jade-include.html" @asset))) 201 | 202 | (it "has the expected dependencies" 203 | (should= ["common.jade" "sub/jade-include.jade" "sub/samedir.jade"] 204 | (sorted-dependencies @asset)))) 205 | 206 | (context "using Jade helpers" 207 | 208 | (with-all context' (-> 209 | @twixt-context 210 | ;; The merge is normally part of the Ring code. 211 | (merge (:twixt-template @options)) 212 | ;; This could be done by Ring middleware, or by 213 | ;; modifying the :twixt-template as well. 214 | (assoc-in [:jade :variables "logoTitle"] "Our Logo"))) 215 | 216 | 217 | (with-all asset (@pipeline "jade-helper.jade" @context')) 218 | 219 | (it "has the correct compiled content ignoring attr order" 220 | (should (or (= "" (read-asset-content @asset)) 221 | (= "" (read-asset-content @asset))))) 222 | 223 | (it "includes a dependency on the asset accessed by twixt.uri()" 224 | (should= ["aviso-logo.png" "jade-helper.jade"] 225 | (sorted-dependencies @asset))) 226 | 227 | 228 | (it "supports multiple assets via twixt.uris()" 229 | (should (have-same-content "expected/jade-uris-helper.html" 230 | (@pipeline "jade-uris-helper.jade" @context')))))) 231 | 232 | (context "Less compilation" 233 | 234 | (context "basic compilation" 235 | (with-all asset (@pipeline "sample.less" @twixt-context)) 236 | 237 | (it "can read and transform a source file" 238 | (should= "text/css" (:content-type @asset)) 239 | (should (:compiled @asset))) 240 | 241 | (it "has the correct compiled content" 242 | (should (have-same-content "expected/sample.css" @asset))) 243 | 244 | (it "has an attached source.map" 245 | (should-have-content (read-resource-content "expected/sample.map") 246 | (read-attachment-content @asset "source.map"))) 247 | 248 | (it "includes dependencies for @import-ed files" 249 | (should= ["colors.less" "sample.less"] 250 | (sorted-dependencies @asset))) 251 | 252 | (it "throws an exception for compilation failures" 253 | (try 254 | (@pipeline "invalid-less.less" @twixt-context) 255 | (should-fail) 256 | (catch Exception e 257 | #_(-> e .getMessage println) 258 | (should (-> e .getMessage (.contains "META-INF/assets/invalid-less.less:3:5: no viable alternative at input 'p'"))))))) 259 | 260 | (context "using data-uri and getData method" 261 | 262 | (with-all asset (@pipeline "logo.less" @twixt-context)) 263 | 264 | (it "has the correct compiled content" 265 | (should (have-same-content "expected/logo.css" @asset))))) 266 | 267 | (context "stack support" 268 | 269 | (context "simple stack" 270 | 271 | (with-all asset (@pipeline "stack/bedrock.stack" @twixt-context)) 272 | 273 | (it "has the content type from the stack file" 274 | (should= "text/javascript" (:content-type @asset))) 275 | 276 | (it "is marked as compiled" 277 | (should (:compiled @asset))) 278 | 279 | (it "identifies the correct aggregated asset paths" 280 | (should= ["stack/fred.js" "stack/barney.js"] 281 | (-> @asset :aggregate-asset-paths))) 282 | 283 | (it "includes dependencies on every file in the stack" 284 | (should= ["stack/barney.js" "stack/bedrock.stack" "stack/fred.js"] 285 | (sorted-dependencies @asset))) 286 | 287 | (it "has the correct aggregated content" 288 | (should (have-same-content "expected/bedrock.js" @asset)))) 289 | 290 | (context "stack with compilation" 291 | 292 | (with-all asset (@pipeline "stack/compiled.stack" @twixt-context)) 293 | 294 | (it "identifies the correct aggregated asset path" 295 | (should= ["coffeescript-source.coffee" "stack/stack.coffee"] 296 | (-> @asset :aggregate-asset-paths))) 297 | 298 | (it "includes dependencies on every file in the stack" 299 | (should= ["coffeescript-source.coffee" "stack/compiled.stack" "stack/stack.coffee"] 300 | (sorted-dependencies @asset))) 301 | 302 | ;; Note: the compiled output includes the sourceMappingURL comments. 303 | ;; It is possible that will not work well in the client and may need to be filtered out. 304 | (it "has the correct aggregated content" 305 | (should (have-same-content "expected/compiled-stack.js" @asset)))) 306 | 307 | (context "stack of stacks" 308 | (with-all asset (@pipeline "stack/meta.stack" @twixt-context)) 309 | 310 | (it "identifies the correct aggregated asset path" 311 | (should= ["stack/fred.js" "stack/barney.js" "coffeescript-source.coffee" "stack/stack.coffee"] 312 | (-> @asset :aggregate-asset-paths))) 313 | 314 | (it "includes dependencies on every file in the stack" 315 | (should= ["coffeescript-source.coffee" "stack/barney.js" "stack/bedrock.stack" "stack/compiled.stack" "stack/fred.js" "stack/meta.stack" "stack/stack.coffee"] 316 | (sorted-dependencies @asset))) 317 | 318 | 319 | (it "has the correct aggregated content" 320 | (should (have-same-content "expected/meta.js" @asset)))) 321 | 322 | (context "stack with missing component" 323 | 324 | (it "throws a reasonable exception when a component is missing" 325 | (try 326 | (get-asset-uri @twixt-context "stack/missing-component.stack") 327 | (should-fail) 328 | (catch Exception e 329 | (should= "Could not locate resource `stack/does-not-exist.coffee' (a component of `stack/missing-component.stack')." 330 | (.getMessage e)))))) 331 | 332 | (context "CSS stack" 333 | 334 | (with-all asset (@pipeline "stack/style.stack" @twixt-context)) 335 | 336 | (it "has the content type from the stack file" 337 | (should= "text/css" (:content-type @asset))) 338 | 339 | (it "identifies the correct aggregated asset paths" 340 | (should= ["stack/local.less" "sample.less"] 341 | (-> @asset :aggregate-asset-paths))) 342 | 343 | (it "identifies the correct dependencies" 344 | ;; Remember that a change to aviso-logo.png will change the checksum in the 345 | ;; compiled and CSS-rewritten stack/local.css, so it must be a dependency 346 | ;; of the final asset. 347 | (should= ["aviso-logo.png" "colors.less" "sample.less" "stack/local.less" "stack/style.stack"] 348 | (sorted-dependencies @asset))) 349 | 350 | ;; Again, need to think about stripping out the sourceMappingURL lines. 351 | (it "contains the correct aggregated content" 352 | (should (have-same-content "expected/style.css" @asset)))) 353 | 354 | (context "get-asset-uri" 355 | (it "always returns the stack asset URI" 356 | (should= "/assets/[hash]/stack/bedrock.stack" 357 | (->> (get-asset-uri @twixt-context "stack/bedrock.stack") 358 | remove-hash-from-uri)))) 359 | 360 | (context "get-asset-uris" 361 | 362 | (it "returns a list of component asset URIs in place of a stack" 363 | 364 | (should= ["/assets/[hash]/stack/fred.js" "/assets/[hash]/stack/barney.js"] 365 | (->> (get-asset-uris @twixt-context "stack/bedrock.stack") 366 | (map remove-hash-from-uri)))) 367 | 368 | (it "returns just the stack asset URI in production mode" 369 | (should= ["/assets/[hash]/stack/bedrock.stack"] 370 | (->> (get-asset-uris (assoc @twixt-context :development-mode false) "stack/bedrock.stack") 371 | (map remove-hash-from-uri)))))) 372 | 373 | (context "JavaScript Minimization" 374 | (context "is enabled by default in production mode" 375 | 376 | (with-all prod-options (-> @options 377 | (assoc :development-mode false 378 | :js-optimizations :default) 379 | (with-sub-cache-dir "js-min-1"))) 380 | 381 | (with-all prod-pipeline (default-asset-pipeline @prod-options)) 382 | 383 | (with-all prod-twixt-context (assoc @prod-options :asset-pipeline @prod-pipeline)) 384 | 385 | (with-all asset (@prod-pipeline "stack/meta.stack" @prod-twixt-context)) 386 | 387 | (it "contains the correct minimized content" 388 | (should (have-same-content "expected/minimized/meta.js" @asset))) 389 | 390 | (it "can handle much larger files" 391 | (should (have-same-content "expected/minimized/bootstrap.js" 392 | (@prod-pipeline "stack/bootstrap.stack" @prod-twixt-context))))) 393 | 394 | (context "can be disabled in production mode" 395 | 396 | (with-all prod-options (-> @options 397 | (assoc :development-mode false 398 | :js-optimizations :none) 399 | (with-sub-cache-dir "js-min-2"))) 400 | 401 | (with-all prod-pipeline (default-asset-pipeline @prod-options)) 402 | 403 | (with-all prod-twixt-context (assoc @prod-options :asset-pipeline @prod-pipeline)) 404 | 405 | (with-all asset (@prod-pipeline "stack/meta.stack" @prod-twixt-context)) 406 | 407 | (it "contains the correct unoptimized content" 408 | (should (have-same-content "expected/meta.js" @asset)))) 409 | 410 | (context "can be enabled in development mode" 411 | 412 | (with-all dev-options (-> @options 413 | (assoc :js-optimizations :simple) 414 | (with-sub-cache-dir "js-min-3"))) 415 | 416 | (with-all dev-pipeline (default-asset-pipeline @dev-options)) 417 | 418 | (with-all dev-twixt-context (assoc @dev-options :asset-pipeline @dev-pipeline)) 419 | 420 | (with-all asset (@dev-pipeline "stack/fred.js" @dev-twixt-context)) 421 | 422 | (it "contains the correct unoptimized content" 423 | (should (have-same-content "expected/minimized/fred.js" @asset))))) 424 | 425 | (context "CSS minification" 426 | (it "can be expicitly enabled" 427 | (let [asset (-> @options 428 | (assoc :minimize-css true) 429 | (with-sub-cache-dir "css-min-1") 430 | (get-asset-with-options "sample.less"))] 431 | (should 432 | (have-same-content "expected/minimized/sample.css" asset)))) 433 | 434 | (it "is enabled by default in production mode" 435 | (let [asset (-> @options 436 | (assoc :development-mode false) 437 | (with-sub-cache-dir "css-min-2") 438 | ;; This is not a terrific idea in that a change to the Bootstrap dependency 439 | ;; will invalidate this test. On the other hand, it ensures (on the side) 440 | ;; that YUICompressor can handle compiling all of Bootstrap. 441 | (get-asset-with-options "bootstrap/less/bootstrap.less"))] 442 | (should 443 | (have-same-content "expected/minimized/bootstrap.css" asset))))) 444 | 445 | (context "asset redirector" 446 | (with-all wrapped (ring/wrap-with-asset-redirector (constantly nil))) 447 | 448 | (with-all request {:uri "/sample.less" :twixt @twixt-context}) 449 | 450 | (with-all response (@wrapped @request)) 451 | 452 | (it "sends a 302 response" 453 | 454 | (should= 302 (:status @response))) 455 | 456 | (it "sends an empty body" 457 | (should= "" (:body @response))) 458 | 459 | (it "sends a proper Location header" 460 | (should (re-matches #"/assets/.*/sample.less" (get-in @response [:headers "Location"])))) 461 | 462 | (it "returns nil for non-matching paths" 463 | (do-template [path] 464 | (should-be-nil (@wrapped {:uri path :twixt @twixt-context})) 465 | "/" 466 | "/a-folder" 467 | "/another/folder/"))) 468 | 469 | (context "exporter" 470 | 471 | (with-all ring-handler (startup/wrap-with-twixt 472 | (-> (fn [request] 473 | (-> request 474 | :twixt 475 | (get-asset-uri "sub/jade-include.jade"))) 476 | (resource/wrap-resource "target/exported")) 477 | (-> twixt/default-options 478 | (assoc-in [:exports :output-dir] "target/exported") 479 | (update-in [:exports :assets] into ["sub/jade-include.jade"])) 480 | true)) 481 | 482 | 483 | (it "can export a file" 484 | (let [response (@ring-handler {:request-method :get 485 | :uri "/sub/jade-include.jade"})] 486 | 487 | 488 | (should-have-content (read-resource-content "expected/jade-include.html") 489 | (-> "target/exported/sub/jade-include.jade" 490 | io/file 491 | read-as-trimmed-string)))) 492 | 493 | (it "exposes the exported alias as the asset URI" 494 | (should= "/sub/jade-include.jade" 495 | (@ring-handler {:request-method :get 496 | :uri "any-match-ok"})))) 497 | 498 | ;; Slightly bogus; this lets the mass of exceptions written out by the executing tests have a chance to finish 499 | ;; before speclj outputs the report; without it, you often get a jumble of console output (including formatted exceptions) 500 | ;; and the report as well. Perhaps another solution is to get speclj to pipe its output through clojure.tools.logging? 501 | (it "needs to slow down to let the console catch up" 502 | (Thread/sleep 1000)) 503 | 504 | (after-all (System/gc))) 505 | 506 | 507 | 508 | (run-specs) -------------------------------------------------------------------------------- /spec/io/aviso/utils_spec.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.utils-spec 2 | (:use 3 | clojure.template 4 | io.aviso.twixt.utils 5 | speclj.core) 6 | (:require 7 | [clojure.java.io :as io])) 8 | 9 | 10 | (defn- get-modified-at 11 | [resource-path] 12 | (-> resource-path io/resource modified-at .getTime)) 13 | 14 | (describe "io.aviso.twixt.utils" 15 | 16 | (context "compute-relative-path" 17 | 18 | (it "can compute relative paths" 19 | 20 | (do-template [start relative expected] 21 | 22 | (should= expected 23 | (compute-relative-path start relative)) 24 | 25 | "foo/bar.gif" "baz.png" "foo/baz.png" 26 | 27 | "foo/bar.gif" "./baz.png" "foo/baz.png" 28 | 29 | "foo/bar.gif" "../zip.zap" "zip.zap" 30 | 31 | "foo/bar/baz/biff.gif" "../gnip/zip.zap" "foo/bar/gnip/zip.zap" 32 | 33 | "foo/bar/gif" "../frozz/pugh.pdf" "foo/frozz/pugh.pdf")) 34 | 35 | 36 | (it "throws IllegalArgumentException if ../ too many times" 37 | (should-throw IllegalArgumentException 38 | (compute-relative-path "foo/bar.png" "../../too-high.pdf")))) 39 | 40 | (context "access to time modified" 41 | 42 | ;; This is to verify some underpinnings 43 | 44 | (it "can access time modified for a local file" 45 | 46 | ;; This will be on the classpath, but on the filesystem since it is part of the current project. 47 | 48 | (should (-> "META-INF/assets/colors.less" 49 | get-modified-at 50 | pos?))) 51 | 52 | (it "can access time modified for a resource in a JAR" 53 | (should (-> "META-INF/leiningen/io.aviso/pretty/LICENSE" 54 | get-modified-at 55 | pos?))))) 56 | 57 | 58 | (run-specs) -------------------------------------------------------------------------------- /src/io/aviso/twixt.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt 2 | "Pipeline for accessing and transforming assets for streaming. 3 | 4 | Twixt integrates with Ring, as a set of Ring request filters. 5 | 6 | Twixt plugs into the Ring pipeline, but most of its work is done in terms of the asset pipeline. 7 | 8 | An asset pipeline handler is passed two values: an asset path and a Twixt context, and returns an asset. 9 | 10 | The asset path is a string, identifying the location of the asset on the classpath, beneath 11 | `META-INF/assets`. 12 | 13 | The Twixt context provides additional information that may be needed 14 | when resolving the asset. 15 | 16 | The asset itself is a map with a specific set of keys. 17 | 18 | As with Ring, there's the concept of middleware; functions that accept an asset handler (plus optional 19 | additional parameters) and return a new asset handler." 20 | (:require [clojure.java.io :as io] 21 | [io.aviso.toolchest.macros :refer [cond-let]] 22 | [io.aviso.tracker :as t] 23 | [io.aviso.twixt 24 | [asset :as asset] 25 | [compress :as compress] 26 | [css-rewrite :as rewrite] 27 | [css-minification :as cssmin] 28 | [fs-cache :as fs] 29 | [memory-cache :as mem] 30 | [js-minification :as js] 31 | [schemas :refer [Asset AssetHandler AssetPath AssetURI TwixtContext ResourcePath]] 32 | [utils :as utils]] 33 | [ring.util.mime-type :as mime] 34 | [schema.core :as s] 35 | [clojure.string :as str]) 36 | (:import [java.net URL] 37 | org.webjars.WebJarAssetLocator 38 | [java.io File])) 39 | 40 | ;;; Lots of stuff from Tapestry 5.4 is not yet implemented 41 | ;;; - multiple domains (the context, the file system, etc.) 42 | ;;; - CSS minification 43 | ;;; - AMD/RequireJS modules 44 | 45 | (defn- extract-file-extension [^String path] 46 | (let [dotx (.lastIndexOf path ".")] 47 | (.substring path (inc dotx)))) 48 | 49 | (defn- extract-content-type 50 | "Uses the resource-path's file extension to identify the content type." 51 | [content-types resource-path] 52 | (get content-types (extract-file-extension resource-path) "application/octet-stream")) 53 | 54 | (s/defn new-asset :- Asset 55 | "Create a new Asset. 56 | 57 | content-types 58 | : The content-types map from the Twixt options. 59 | 60 | asset-path 61 | : The path to the asset from the asset Root. This may be used in some tracking/debugging output. 62 | 63 | resource-path 64 | : The path to the asset on the classpath, used to locate the Asset's raw content. 65 | 66 | url 67 | : A URL used to access the raw content for the asset." 68 | {:size "0.1.17"} 69 | [content-types :- {s/Str s/Str} 70 | asset-path :- AssetPath 71 | resource-path :- ResourcePath 72 | url :- URL] 73 | (let [^bytes content-bytes (utils/read-content url) 74 | checksum (utils/compute-checksum content-bytes) 75 | modified-at (utils/modified-at url)] 76 | {:asset-path asset-path 77 | :resource-path resource-path 78 | :modified-at modified-at 79 | :content-type (extract-content-type content-types resource-path) 80 | :content content-bytes 81 | :size (alength content-bytes) 82 | :checksum checksum 83 | :dependencies {resource-path {:asset-path asset-path 84 | :checksum checksum 85 | :modified-at modified-at}}})) 86 | 87 | (s/defn make-asset-resolver :- AssetHandler 88 | "Factory for the standard Asset resolver function which converts a path into an Asset. 89 | 90 | The factory function is passed the Twixt options, which defines content types mappings. 91 | 92 | The resolver function is passed an asset path and a pipeline context (which is ignored); 93 | The asset path is converted to a classpath resource via the configuration; 94 | if the resource exists, it is converted to an Asset. 95 | 96 | If the Asset does not exist, the resolver returns nil. 97 | 98 | An Asset has the minimum following keys: 99 | 100 | :content 101 | : content of the asset in a form that is compatible with clojure.java.io 102 | 103 | :asset-path 104 | : path of the asset under the root folder /META-INF/assets/ 105 | 106 | :resource-path 107 | : full path of the underlying resource 108 | 109 | :content-type 110 | : MIME type of the content, as determined from the path's extension 111 | 112 | :size 113 | : size of the asset in bytes 114 | 115 | :checksum 116 | : Adler32 checksum of the content 117 | 118 | :modified-at 119 | : instant at which the file was last modified (not always trustworthy for files packaged in JARs) 120 | 121 | :compiled 122 | : _optional_ - true for assets that represent some form of compilation (or aggregation) and should be cached 123 | 124 | :aggregate-asset-paths 125 | : _optional_ - seq of asset paths from which a stack asset was constructed 126 | 127 | :dependencies 128 | : _optional_ - used to track underlying dependencies; a map of resource path to details about that resource 129 | (keys :asset-path, :checksum, and :modified-at) 130 | 131 | :attachments 132 | : _optional_ - map of string name to attachment (with keys :content, :size, and :content-type)" 133 | [twixt-options] 134 | (let [{:keys [content-types]} twixt-options] 135 | (fn [asset-path _context] 136 | (let [resource-path (str "META-INF/assets/" asset-path)] 137 | (if-let [url (io/resource resource-path)] 138 | (new-asset content-types asset-path resource-path url)))))) 139 | 140 | (s/defn make-webjars-asset-resolver :- AssetHandler 141 | "As with [[make-asset-resolver]], but finds assets inside WebJars. 142 | 143 | The path must start with a WebJar libary name, such as \"bootstrap\". 144 | The version number is injected into the resource path." 145 | {:since "0.1.17"} 146 | [twixt-options] 147 | (let [webjar-versions (into {} (-> (WebJarAssetLocator.) .getWebJars)) 148 | content-types (:content-types twixt-options)] 149 | (fn [asset-path _context] 150 | (cond-let 151 | [[webjar-name webjar-asset-path] (str/split asset-path #"/" 2) 152 | webjar-version (get webjar-versions webjar-name)] 153 | 154 | (nil? webjar-version) 155 | nil 156 | 157 | [resource-path (str "META-INF/resources/webjars/" webjar-name "/" webjar-version "/" webjar-asset-path) 158 | url (io/resource resource-path)] 159 | 160 | (some? url) 161 | (new-asset content-types asset-path resource-path url))))) 162 | 163 | 164 | (def default-options 165 | "Provides the default options when using Twixt; these rarely need to be changed except, perhaps, for :path-prefix 166 | or :cache-folder, or by plugins." 167 | {:path-prefix "/assets/" 168 | :content-types mime/default-mime-types 169 | :resolver-factories [make-asset-resolver make-webjars-asset-resolver] 170 | ;; Content transformer, e.g., compilers (such as CoffeeScript to JavaScript). Key is a content type, 171 | ;; value is a function passed an asset and Twixt context, and returns a new asset. 172 | :content-transformers {} 173 | ;; Identify which content types are compressable; all other content types are assumed to not be compressable. 174 | :compressable #{"text/*" "application/edn" "application/json"} 175 | :js-optimizations :default 176 | :cache {:cache-dir (System/getProperty "twixt.cache-dir" (System/getProperty "java.io.tmpdir")) 177 | :check-interval-ms 1000} 178 | :exports {:interval-ms 5000 179 | :output-dir "resources/public" 180 | :output-uri "" 181 | :assets []}}) 182 | 183 | 184 | (s/defn find-asset :- (s/maybe Asset) 185 | "Looks for a particular asset by asset path, returning the asset (if found)." 186 | {:added "0.1.17"} 187 | [asset-path :- AssetPath 188 | context :- TwixtContext] 189 | ((:asset-pipeline context) asset-path context)) 190 | 191 | (s/defn asset->uri :- s/Str 192 | "Converts an Asset to a URI that can be provided to a client." 193 | {:added "0.1.17"} 194 | [context :- TwixtContext 195 | asset :- Asset] 196 | (if-let [alias-uri (get (:asset-aliases context) (:asset-path asset))] 197 | alias-uri 198 | (asset/asset->request-path (:path-prefix context) asset))) 199 | 200 | (defn- get-single-asset 201 | [asset-pipeline context asset-path] 202 | {:pre [(some? asset-pipeline) 203 | (some? context) 204 | (some? asset-path)]} 205 | (or (asset-pipeline asset-path context) 206 | (throw (ex-info (format "Asset path `%s' does not map to an available resource." asset-path) 207 | context)))) 208 | 209 | (s/defn get-asset-uris :- [AssetURI] 210 | "Converts a number of asset paths into client URIs. 211 | Each path must exist. 212 | 213 | An asset path does not start with a leading slash. 214 | The default asset resolver locates each asset on the classpath under `META-INF/assets/`. 215 | 216 | Unlike [[get-asset-uri]], this function will (in _development mode_) expand stack assets 217 | into the asset URIs for all component assets. 218 | 219 | context 220 | : the :twixt key, extracted from the Ring request map 221 | 222 | paths 223 | : asset paths to convert to URIs" 224 | [{:keys [asset-pipeline development-mode] :as context} :- TwixtContext 225 | & paths :- [AssetPath]] 226 | (loop [asset-uris [] 227 | [path & more-paths] paths] 228 | (if-not (some? path) 229 | asset-uris 230 | (let [asset (get-single-asset asset-pipeline context path) 231 | aggregate-asset-paths (-> asset :aggregate-asset-paths seq)] 232 | (if (and development-mode aggregate-asset-paths) 233 | (recur (into asset-uris (apply get-asset-uris context aggregate-asset-paths)) 234 | more-paths) 235 | (recur (conj asset-uris (asset->uri context asset)) 236 | more-paths)))))) 237 | 238 | (s/defn get-asset-uri :- AssetURI 239 | "Converts a single asset paths into a client URI. 240 | Throws an exception if the path does not exist. 241 | 242 | The default asset resolver locates each asset on the classpath under `META-INF/assets/`. 243 | 244 | This works much the same as [[get-asset-uris]] except that stack asset will 245 | be a URI to the stack (whose content is the aggregation of the components of the stack) 246 | rather than the list of component asset URIs. 247 | This matches the behavior of `get-asset-uris` in _production_, but you should use 248 | `get-asset-uris` in development, since in development, you want the individual 249 | component assets, rather than the aggregated whole. 250 | 251 | context 252 | : the :twixt key, extracted from the Ring request map 253 | 254 | asset-path 255 | : path to the asset; asset paths do __not__ start with a leading slash" 256 | [context :- TwixtContext 257 | asset-path :- AssetPath] 258 | (let [{:keys [asset-pipeline]} context] 259 | (->> 260 | asset-path 261 | (get-single-asset asset-pipeline context) 262 | (asset->uri context)))) 263 | 264 | (s/defn find-asset-uri :- (s/maybe AssetURI) 265 | "Returns the URI for an asset, if it exists. 266 | If not, returns nil." 267 | [{:keys [asset-pipeline] :as context} 268 | asset-path :- AssetPath] 269 | (if-let [asset (asset-pipeline asset-path context)] 270 | (asset->uri context asset))) 271 | 272 | (s/defn wrap-pipeline-with-tracing :- AssetHandler 273 | "The first middleware in the asset pipeline, used to trace the construction of the asset." 274 | [asset-handler :- AssetHandler] 275 | (fn [asset-path context] 276 | (t/track 277 | #(format "Accessing asset `%s'" asset-path) 278 | (asset-handler asset-path context)))) 279 | 280 | (s/defn wrap-pipeline-with-per-content-type-transformation :- AssetHandler 281 | [asset-handler :- AssetHandler 282 | {:keys [content-transformers]}] 283 | (fn [asset-path context] 284 | (let [asset (asset-handler asset-path context) 285 | content-type (:content-type asset) 286 | transformer (get content-transformers content-type)] 287 | (if transformer 288 | (transformer asset context) 289 | asset)))) 290 | 291 | (s/defn default-wrap-pipeline-with-content-transformation :- AssetHandler 292 | "Used when constructing the asset pipeline, wraps a handler (normally, the asset resolver) 293 | with additional pipeline handlers based on 294 | the `:content-transformers` key of the Twixt options, plus JavaScript minification and CSS URL Rewriting 295 | and CSS Minification. 296 | 297 | The JavaScript and/or CSS minification may not be enabled in development mode." 298 | [asset-handler :- AssetHandler 299 | twixt-options] 300 | (-> 301 | asset-handler 302 | (wrap-pipeline-with-per-content-type-transformation twixt-options) 303 | (js/wrap-with-javascript-minimizations twixt-options) 304 | rewrite/wrap-with-css-rewriting 305 | (cssmin/wrap-with-css-minification twixt-options))) 306 | 307 | (s/defn default-wrap-pipeline-with-caching :- AssetHandler 308 | "Used when constructing the asset pipeline to wrap the handler with file system caching 309 | of compiled assets, and in-memory caching of all assets. 310 | 311 | This is invoked before adding support for compression." 312 | [asset-handler :- AssetHandler 313 | cache-dir :- File 314 | check-interval-ms :- s/Num] 315 | (-> 316 | asset-handler 317 | ;; The file system cache should come after anything downstream that might compile. 318 | (fs/wrap-with-filesystem-cache cache-dir) 319 | ;; This in-memory cache prevents constant checks agains the file system cache, or against 320 | ;; the asset resolvers. 321 | (mem/wrap-with-memory-cache check-interval-ms))) 322 | 323 | (s/defn wrap-pipeline-with-asset-resolver :- AssetHandler 324 | "Wraps the asset handler so that the :asset-resolver key is set to the asset resolver; the asset resolver 325 | is a way to bypass intermediate steps and gain access to the asset in its completely untransformed format." 326 | [asset-handler :- AssetHandler 327 | asset-resolver] 328 | (fn [asset-path context] 329 | (asset-handler asset-path (assoc context :asset-resolver asset-resolver)))) 330 | 331 | (defn- merge-handlers [handlers] 332 | (fn [asset-path context] 333 | (loop [[h & more-handlers] handlers] 334 | (cond-let 335 | (nil? h) 336 | nil 337 | 338 | [result (h asset-path context)] 339 | 340 | (some? result) 341 | result 342 | 343 | :else 344 | (recur more-handlers))))) 345 | 346 | (s/defn default-asset-pipeline :- AssetHandler 347 | "Sets up the default pipeline. 348 | 349 | The asset pipeline starts with a resolver, which is then intercepted using asset pipeline middleware. 350 | As with Ring, middleware is a function that accepts an asset-handler and returns an asset-handler. The asset-handler 351 | is passed an asset path and a context. The initial context is the value of the `:twixt` key from the 352 | Ring request map. 353 | 354 | In production mode, JavaScript will be minimized. 355 | 356 | The context will contain an :asset-pipeline key whose value is the asset pipeline in use. 357 | The context will contain a :path-prefix key, extracted from the twixt options. 358 | The context may also be passed to [[get-asset-uri]] (and related functions). 359 | 360 | In some cases, middlware may modify the context before passing it forward to the next asset-handler, typically 361 | by adding additional keys." 362 | [twixt-options] 363 | (let [{:keys [development-mode resolver-factories]} twixt-options 364 | asset-resolvers (for [factory resolver-factories] 365 | (factory twixt-options)) 366 | asset-resolver (merge-handlers asset-resolvers) 367 | ^String cache-path (-> twixt-options :cache :cache-dir) 368 | cache-dir (File. cache-path 369 | (if development-mode "dev" "prod")) 370 | check-interval-ms (-> twixt-options :cache :check-interval-ms)] 371 | (-> 372 | asset-resolver 373 | (default-wrap-pipeline-with-content-transformation twixt-options) 374 | (default-wrap-pipeline-with-caching cache-dir check-interval-ms) 375 | (compress/wrap-with-gzip-compression twixt-options) 376 | (wrap-pipeline-with-asset-resolver asset-resolver) 377 | wrap-pipeline-with-tracing))) 378 | 379 | -------------------------------------------------------------------------------- /src/io/aviso/twixt/asset.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.asset 2 | "Utilities for dealing with a Twixt asset map.") 3 | 4 | (defn asset->request-path 5 | "Computes the complete asset path that can be referenced by a client in order to obtain 6 | the asset content. This includes the path prefix, the checksum, and the asset path itself. Compressed assets 7 | have their checksum prefixed with 'z'." 8 | [path-prefix asset] 9 | (str path-prefix 10 | (if (:compressed asset) "z" "") 11 | (:checksum asset) 12 | "/" 13 | (:asset-path asset))) 14 | -------------------------------------------------------------------------------- /src/io/aviso/twixt/coffee_script.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.coffee-script 2 | "Provides asset pipeline middleware to perform CoffeeScript to JavaScript compilation. 3 | 4 | CoffeeScript assets include a source map as an attachment. 5 | The source map includes the source of the CoffeeScript file. 6 | 7 | The compiled JavaScript includes the directive for the browser to load the source map." 8 | (:import 9 | [java.util Map]) 10 | (:require 11 | [io.aviso.tracker :as t] 12 | [io.aviso.twixt 13 | [rhino :as rhino] 14 | [utils :as utils]])) 15 | 16 | (defn- ^String extract-value [^Map object key] 17 | (str (.get object key))) 18 | 19 | (defn- coffee-script-compiler [asset context] 20 | (let [file-path (:resource-path asset) 21 | file-name (utils/path->name file-path)] 22 | (t/timer 23 | #(format "Compiled `%s' to JavaScript in %.2f ms" file-path %) 24 | (t/track 25 | #(format "Compiling `%s' to JavaScript" file-path) 26 | (let [^Map result 27 | (rhino/invoke-javascript ["META-INF/twixt/coffee-script.js" "META-INF/twixt/invoke-coffeescript.js"] 28 | "compileCoffeeScriptSource" 29 | (-> asset :content utils/as-string) 30 | file-path 31 | file-name)] 32 | 33 | ;; The script returns an object with key "exception" or key "output": 34 | (when (.containsKey result "exception") 35 | (throw (RuntimeException. (extract-value result "exception")))) 36 | (-> asset 37 | (utils/create-compiled-asset "text/javascript" (extract-value result "output") nil) 38 | (utils/add-attachment "source.map" "application/json" (-> result (extract-value "sourceMap") utils/as-bytes)))))))) 39 | 40 | (defn register-coffee-script 41 | "Updates the Twixt options with support for compiling CoffeeScript into JavaScript." 42 | [options] 43 | (-> options 44 | (assoc-in [:content-types "coffee"] "text/coffeescript") 45 | (assoc-in [:content-transformers "text/coffeescript"] coffee-script-compiler))) -------------------------------------------------------------------------------- /src/io/aviso/twixt/compress.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.compress 2 | "Asset pipeline middleware for handling compressable assets." 3 | (:require [clojure.string :as str] 4 | [clojure.java.io :as io] 5 | [io.aviso.twixt 6 | [memory-cache :as mem] 7 | [utils :as utils] 8 | [schemas :refer [AssetHandler]]] 9 | [schema.core :as s]) 10 | (:import [java.io ByteArrayOutputStream] 11 | [java.util.zip GZIPOutputStream])) 12 | 13 | (defn- make-wildcard [^String mime-type] 14 | (-> 15 | mime-type 16 | (.split "/") 17 | first 18 | (str "/*"))) 19 | 20 | (defn- is-compressable-mime-type? 21 | [compressable-types mime-type] 22 | (or 23 | (compressable-types mime-type) 24 | (compressable-types (make-wildcard mime-type)))) 25 | 26 | (defn- is-compressable? 27 | [compressable-types asset] 28 | (is-compressable-mime-type? compressable-types (:content-type asset))) 29 | 30 | (defn- compress-asset 31 | [asset] 32 | (let [bos (ByteArrayOutputStream. (:size asset))] 33 | (with-open [gz (GZIPOutputStream. bos) 34 | is (io/input-stream (:content asset))] 35 | (io/copy is gz)) 36 | (-> asset 37 | (utils/replace-asset-content (:content-type asset) (.toByteArray bos)) 38 | (assoc :compressed true)))) 39 | 40 | (defn- is-gzip-supported? 41 | [request] 42 | (if-let [encodings (-> request :headers (get "accept-encoding"))] 43 | (some #(.equalsIgnoreCase ^String % "gzip") 44 | (str/split encodings #",")))) 45 | 46 | (s/defn wrap-with-gzip-compression :- AssetHandler 47 | "Adds an in-memory cache that includes invalidation checks. 48 | 49 | Although compression is expensive enough that it is not desirable to 50 | perform compression on every request, it is also overkill to 51 | commit the compressed asset to a file system cache. 52 | 53 | The cache is only consulted if the :gzip-enabled options key is true." 54 | {:added "0.1.20"} 55 | [asset-handler :- AssetHandler 56 | twixt-options] 57 | (let [cache (mem/wrap-with-transforming-cache asset-handler 58 | (partial is-compressable? (:compressable twixt-options)) 59 | compress-asset)] 60 | (fn [asset-path options] 61 | (let [delegate (if (:gzip-enabled options) cache asset-handler)] 62 | (delegate asset-path options))))) 63 | 64 | (defn wrap-with-compression-analyzer 65 | "Ring middleware that analyzes the incoming request to determine if the 66 | client can accept compressed streams." 67 | [handler] 68 | (fn [request] 69 | (handler (assoc-in request [:twixt :gzip-enabled] (is-gzip-supported? request))))) -------------------------------------------------------------------------------- /src/io/aviso/twixt/css_minification.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.css-minification 2 | "CSS Miniification based on YUICompressor." 3 | {:added "0.1.20"} 4 | (:require [schema.core :as s] 5 | [io.aviso.twixt.schemas :refer [AssetHandler]] 6 | [io.aviso.tracker :as t] 7 | [clojure.java.io :as io] 8 | [io.aviso.twixt.utils :as utils]) 9 | (:import [java.io StringWriter] 10 | [com.yahoo.platform.yui.compressor CssCompressor])) 11 | 12 | 13 | (defn- run-minimizer 14 | [{:keys [resource-path content] :as asset}] 15 | (t/timer 16 | #(format "Minimized `%s' (%,d bytes) in %.2f ms" 17 | resource-path (:size asset) %) 18 | (t/track 19 | #(format "Minimizing `%s' using YUICompressor." resource-path) 20 | (let [compressed (with-open [writer (StringWriter. 1000) 21 | reader (io/reader content)] 22 | (doto (CssCompressor. reader) 23 | (.compress writer -1)) 24 | 25 | (.flush writer) 26 | (-> writer .getBuffer str))] 27 | (utils/create-compiled-asset asset "text/css" compressed nil))))) 28 | 29 | (s/defn wrap-with-css-minification :- AssetHandler 30 | "Enabled CSS minification based on the :compress-css key of the Twixt options. 31 | If the key is not present, the default is to enable compression only in production mode. 32 | 33 | When not enabled, returns the provided handler unchanged." 34 | [handler :- AssetHandler {:keys [development-mode] :as twixt-options}] 35 | (if (get twixt-options :minimize-css (not development-mode)) 36 | (fn [asset-path {:keys [for-aggregation] :as context}] 37 | (let [{:keys [content-type] :as asset} (handler asset-path context)] 38 | (if (and (= "text/css" content-type) 39 | (not for-aggregation)) 40 | (run-minimizer asset) 41 | asset))) 42 | handler)) -------------------------------------------------------------------------------- /src/io/aviso/twixt/css_rewrite.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.css-rewrite 2 | "Defines an asset pipeline filter used to rewrite URIs in CSS files. 3 | Relative URLs in CSS files will become invalid due to the inclusion of the individual asset's checksum in the 4 | asset request path; instead, the relative URIs are expanded to complete URIs that include the individual asset's checksum." 5 | (:require [clojure.string :as str] 6 | [io.aviso.tracker :as t] 7 | [io.aviso.twixt 8 | [asset :as asset] 9 | [utils :as utils] 10 | [schemas :refer [AssetHandler]]] 11 | [schema.core :as s])) 12 | 13 | (def ^:private url-pattern #"url\(\s*(['\"]?)(.+?)([\#\?].*?)?\1\s*\)") 14 | (def ^:private complete-url-pattern #"^[#/]|(\p{Alpha}\w*:)") 15 | 16 | (defn- rewrite-relative-url 17 | [asset-path context dependencies ^String relative-url] 18 | (if (.startsWith relative-url "data:") 19 | relative-url 20 | (t/track 21 | #(format "Rewriting relative URL `%s'" relative-url) 22 | (let [referenced-path (utils/compute-relative-path asset-path relative-url) 23 | asset ((:asset-pipeline context) referenced-path context)] 24 | (if-not asset 25 | (throw (ex-info 26 | (format "Unable to locate asset `%s'." referenced-path) 27 | {:source-asset asset-path 28 | :relative-url relative-url}))) 29 | 30 | (swap! dependencies utils/add-asset-as-dependency asset) 31 | (asset/asset->request-path (:path-prefix context) asset))))) 32 | 33 | (defn- css-url-match-handler [asset-path context dependencies match] 34 | (let [url (nth match 2) 35 | parameters (nth match 3)] 36 | (str "url(\"" 37 | (if (re-matches complete-url-pattern url) 38 | (str url parameters) 39 | (str (rewrite-relative-url asset-path context dependencies url) 40 | parameters)) 41 | "\")"))) 42 | 43 | (defn- rewrite-css 44 | [asset context] 45 | (t/track 46 | #(format "Rewriting URLs in `%s'" (:asset-path asset)) 47 | (let [content (utils/as-string (:content asset)) 48 | ;; Using an atom this way is clumsy. 49 | dependencies (atom (:dependencies asset)) 50 | content' (str/replace content 51 | url-pattern 52 | (partial css-url-match-handler (:asset-path asset) context dependencies))] 53 | (-> 54 | asset 55 | (utils/replace-asset-content "text/css" (utils/as-bytes content')) 56 | (assoc :dependencies @dependencies))))) 57 | 58 | (s/defn wrap-with-css-rewriting :- AssetHandler 59 | "Wraps the asset handler with the CSS URI rewriting logic needed for the client to be able to properly request the referenced assets. 60 | 61 | Rewriting occurs for individual CSS assets (including those created by compiling a Less source). It does not occur for 62 | aggregated CSS assets (since the individual assets will already have had URIs rewritten)." 63 | [handler :- AssetHandler] 64 | (fn [asset-path context] 65 | (let [asset (handler asset-path context)] 66 | (if (and 67 | (= "text/css" (:content-type asset)) 68 | (-> asset :aggregate-asset-paths empty?)) 69 | (rewrite-css asset context) 70 | asset)))) 71 | -------------------------------------------------------------------------------- /src/io/aviso/twixt/exceptions.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.exceptions 2 | "Support for generating pretty and useful HTML reports when server-side exceptions occur." 3 | (:use hiccup.core 4 | hiccup.page 5 | ring.util.response) 6 | (:require [clojure.string :as s] 7 | [io.aviso 8 | [exception :as exception] 9 | [twixt :as t]]) 10 | (:import [clojure.lang APersistentMap Sequential PersistentHashMap] 11 | [java.util Map] 12 | [java.util.regex Pattern])) 13 | 14 | (def ^:private path-separator (System/getProperty "path.separator")) 15 | 16 | (defn- exception-message 17 | [^Throwable exception] 18 | (or (.getMessage exception) 19 | (-> exception .getClass .getName))) 20 | 21 | (defprotocol MarkupGeneration 22 | "Used to convert arbitrary values into markup strings. 23 | 24 | Extended onto nil, `String`, `APersistentMap`, `Sequential` and `Object`." 25 | (to-markup 26 | [value] 27 | "Returns HTML markup representing the value.")) 28 | 29 | (extend-type nil 30 | MarkupGeneration 31 | (to-markup [_] "nil")) 32 | h 33 | (extend-type String 34 | MarkupGeneration 35 | (to-markup [value] (h value))) 36 | 37 | (extend-type Object 38 | MarkupGeneration 39 | (to-markup [value] (-> value .toString h))) 40 | 41 | (extend-type APersistentMap 42 | MarkupGeneration 43 | (to-markup 44 | [m] 45 | (html 46 | (if (empty? m) 47 | [:em "empty map"] 48 | [:dl 49 | (apply concat 50 | (for [[k v] (sort-by str m)] 51 | [[:dt (to-markup k)] [:dd (to-markup v)]] 52 | )) 53 | ])))) 54 | 55 | (defn- seq->markup [coll] 56 | ;; Since *print-length* is normally nil, we provide an actual limit 57 | (let [limit (or *print-length* 10) 58 | values (take (inc limit) coll) 59 | trimmed (if (<= (count values) limit) 60 | values 61 | (concat 62 | (take limit values) 63 | ["..."]))] 64 | (for [v trimmed] 65 | [:li (to-markup v)]))) 66 | 67 | (extend-type Sequential 68 | MarkupGeneration 69 | (to-markup 70 | [coll] 71 | (html 72 | (if (empty? coll) 73 | [:em "none"] 74 | [:ul (seq->markup coll)])))) 75 | 76 | (extend-type Map 77 | MarkupGeneration 78 | (to-markup 79 | [^Map m] 80 | (to-markup (PersistentHashMap/create m)))) 81 | 82 | (defn- element->clojure-name [element] 83 | (let [names (:names element)] 84 | (list 85 | (s/join "/" (drop-last names)) 86 | "/" 87 | [:strong (last names)]))) 88 | 89 | (defn- element->java-name 90 | [{:keys [package class simple-class method]}] 91 | (list 92 | (if package 93 | (list 94 | [:span.package-name package] 95 | "." 96 | simple-class) 97 | class) 98 | " — " 99 | method)) 100 | 101 | (defn- apply-stack-frame-filter 102 | [filter frames] 103 | (loop [result [] 104 | [frame & more-frames] frames] 105 | (if (nil? frame) 106 | result 107 | (case (filter frame) 108 | :show 109 | (recur (conj result frame) more-frames) 110 | 111 | ;; We could perhaps do this differntly, but unlike console output, we treat these the same. 112 | (:hide :omit) 113 | (recur (conj result (assoc frame :filtered true)) more-frames) 114 | 115 | :terminate 116 | result)))) 117 | 118 | (defn- stack-frame->row-markup 119 | [{:keys [file line names] 120 | :as frame}] 121 | (let [clojure? (-> names empty? not) 122 | java-name (element->java-name frame)] 123 | [:tr (if (:filtered frame) {:class :filtered} {}) 124 | [:td.function-name 125 | (if clojure? 126 | (list 127 | (element->clojure-name frame) 128 | [:div.filtered java-name]) 129 | java-name) 130 | ] 131 | [:td.source-location file (if line ":")] 132 | [:td (or line "")] 133 | ])) 134 | 135 | (defn- single-exception->markup 136 | "Given an analyzed exception, generate markup for it." 137 | [twixt 138 | {^Throwable e :exception 139 | :keys [class-name message properties root]}] 140 | (html 141 | [:div.panel.panel-default 142 | [:div.panel-heading 143 | [:h3.panel-title class-name] 144 | ] 145 | [:div.panel-body 146 | [:h4 message] 147 | ;; TODO: sorting 148 | (when-not (empty? properties) 149 | [:dl 150 | (apply concat 151 | (for [[k v] (sort-by str properties)] 152 | [[:dt (-> k h)] [:dd (to-markup v)]])) 153 | ])] 154 | (if root 155 | (list 156 | [:div.btn-toolbar.spacing-below 157 | [:button.btn.btn-default.btn-sm {:data-action :toggle-filter} "Toggle Stack Frame Filter"]] 158 | [:table.table.table-hover.table-condensed.table-striped 159 | (->> 160 | e 161 | exception/expand-stack-trace 162 | (apply-stack-frame-filter (:stack-frame-filter twixt)) 163 | (map stack-frame->row-markup)) 164 | ])) 165 | ])) 166 | 167 | 168 | (defn exception->markup 169 | "Returns the markup (as a string) for a root exception and its stack of causes, 170 | including a stack trace for the deepest exception." 171 | [twixt root-exception] 172 | (->> 173 | root-exception 174 | exception/analyze-exception 175 | (map (partial single-exception->markup twixt)) 176 | (apply str))) 177 | 178 | (defn- is-path? 179 | [^String k ^String v] 180 | (and (.endsWith k ".path") 181 | (.contains v path-separator))) 182 | 183 | (defn- split-string [^String s ^String sep] 184 | (s/split s (-> sep Pattern/quote Pattern/compile))) 185 | 186 | (defn- path->markup [^String path] 187 | `[:ul 188 | ~@(for [v (.split path path-separator)] 189 | [:li v])]) 190 | 191 | (defn- sysproperties->markup 192 | [] 193 | (let [props (System/getProperties)] 194 | [:dl 195 | (apply concat 196 | (for [k (-> props keys sort) 197 | :let [v (.get props k)]] 198 | [[:dt k] 199 | [:dd (if (is-path? k v) (path->markup v) v)]]))])) 200 | 201 | (defn build-report 202 | "Builds an HTML exception report (as a string). 203 | 204 | request 205 | : Ring request map, which must contain the `:twixt` key 206 | 207 | exception 208 | : instance of Throwable to report" 209 | [request exception] 210 | (let [twixt (:twixt request)] 211 | (html5 212 | [:head 213 | [:title "Exception"] 214 | (apply include-css (t/get-asset-uris twixt 215 | "bootstrap/css/bootstrap.css" 216 | "twixt/exception.less"))] 217 | [:body.hide-filtered 218 | [:div.container 219 | [:div.panel.panel-danger 220 | [:div.panel-heading 221 | [:h3.panel-title "An unexpected exception has occurred."]] 222 | [:div.panel-body 223 | [:h3 (h (exception-message exception))] 224 | ] 225 | ] 226 | (exception->markup twixt exception) 227 | 228 | [:h3 "Request"] 229 | 230 | (to-markup request) 231 | 232 | [:h3 "System Properties"] 233 | (sysproperties->markup) 234 | ] 235 | (apply include-js (t/get-asset-uris twixt 236 | "jquery/jquery.js" 237 | "twixt/exception.coffee")) 238 | ]))) 239 | 240 | (defn wrap-with-exception-reporting 241 | "Wraps the handler to report any uncaught exceptions as an HTML exception report. 242 | This wrapper should wrap around other handlers (including the Twixt handler itself), but be nested within 243 | the twixt-setup handler (which provides the `:twixt` request map key)." 244 | [handler] 245 | (fn [request] 246 | (try 247 | (handler request) 248 | (catch Throwable t 249 | (-> 250 | (build-report request t) 251 | response 252 | (content-type "text/html") 253 | (status 500)))))) 254 | 255 | 256 | (defn default-stack-frame-filter 257 | "The default stack frame filter function, used by the HTML excepton report to identify frames that can be hidden 258 | by default. 259 | 260 | This implementation extends the standard frame filter (`io.aviso.repl/standard-frame-filter`), 261 | to also omit frames with no line number. 262 | 263 | The HTML exception report treats `:omit` and `:hide` identically; frames marked as either are 264 | initially hidden, but can be revealed in the client." 265 | [frame] 266 | (cond 267 | (nil? (:line frame)) 268 | :omit 269 | 270 | :else 271 | (exception/*default-frame-filter* frame))) 272 | 273 | (defn register-exception-reporting 274 | "Must be invoked to configure the Twixt options with the default `:stack-frame-filter` key, [[default-stack-frame-filter]]." 275 | [options] 276 | (assoc options :stack-frame-filter default-stack-frame-filter)) 277 | 278 | -------------------------------------------------------------------------------- /src/io/aviso/twixt/export.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.export 2 | "Support for exporting assets to the file system. 3 | 4 | Ideally, exporting would be fully asynchronous; however that would require a dedicated thread, and 5 | that is a problem as there is no lifecycle for 6 | a Ring request handler to know when the Ring server is itself shutdown." 7 | {:added "0.1.17"} 8 | (:require [schema.core :as s] 9 | [io.aviso.twixt.schemas :refer [AssetPath ExportsConfiguration]] 10 | [io.aviso.tracker :as t] 11 | [io.aviso.toolchest.macros :refer [cond-let]] 12 | [io.aviso.twixt :as twixt] 13 | [clojure.java.io :as io] 14 | [clojure.tools.logging :as l])) 15 | 16 | (deftype ^:private Token [running? 17 | ^Thread thread] 18 | 19 | Object 20 | (finalize [_] 21 | (reset! running? false) 22 | (.interrupt thread))) 23 | 24 | (defn- export-asset 25 | [output-dir {:keys [asset-path content]} output-alias] 26 | (t/track 27 | (if (= asset-path output-alias) 28 | (format "Exporting `%s'." asset-path) 29 | (format "Exporting `%s' (as `%s')." asset-path output-alias)) 30 | 31 | (let [output-file (io/file output-dir output-alias)] 32 | (when-not (.exists output-file) 33 | (-> output-file .getParentFile .mkdirs)) 34 | 35 | (io/copy content output-file)))) 36 | 37 | (defn- check-exports 38 | [context output-dir assets checksums] 39 | (t/track 40 | "Checking exported assets for changes." 41 | (let [context' (assoc context :gzip-enabled false)] 42 | (doseq [[asset-path output-alias] assets] 43 | (try 44 | (t/track 45 | (format "Checking `%s' for changes." asset-path) 46 | (cond-let 47 | [asset (twixt/find-asset asset-path context')] 48 | 49 | (nil? asset) 50 | nil 51 | 52 | [asset-checksum (:checksum asset)] 53 | 54 | (= asset-checksum (get @checksums asset-path)) 55 | nil 56 | 57 | :else 58 | (do 59 | (export-asset output-dir asset output-alias) 60 | (swap! checksums assoc asset-path asset-checksum))) 61 | (catch Throwable _ 62 | ;; Reported by the tracker and ignored. 63 | ))))))) 64 | 65 | 66 | (defn- start-exporter-thread 67 | [context {:keys [interval-ms output-dir assets]}] 68 | (let [checksums (atom {}) 69 | ;; We want the assets to all be the same shape: asset path and output path. 70 | assets' (map #(if (string? %) 71 | [% %] 72 | %) 73 | assets) 74 | running? (atom true) 75 | first-pass (promise) 76 | export-body (fn [] 77 | (l/info "Twixt asset export thread started.") 78 | (while @running? 79 | (try 80 | (check-exports context output-dir assets' checksums) 81 | (deliver first-pass true) 82 | (Thread/sleep interval-ms) 83 | 84 | (catch Throwable _)) 85 | ;; Real errors are reported inside check-exports, and InterruptedException is ignored. 86 | ) 87 | (l/info "Shutting down Twixt asset export thread.")) 88 | export-thread (doto (Thread. ^Runnable export-body) 89 | (.setName "Twixt Export") 90 | (.setDaemon true) 91 | .start)] 92 | ;; Wait for completion of first export pass: 93 | 94 | @first-pass 95 | 96 | ;; The token is returned to the call, which keeps it in an atom. At shutdown time, 97 | ;; the entire Ring request pipeline will be GC'ed, at which point the token will become 98 | ;; weakly referenced. 99 | (Token. running? export-thread))) 100 | 101 | 102 | (defn- build-asset-aliases 103 | [assets output-uri] 104 | (reduce (fn [result asset] 105 | (if (string? asset) 106 | (assoc result asset (str output-uri "/" asset)) 107 | (let [[asset-path output-alias] asset] 108 | (assoc result asset-path (str output-uri "/" output-alias))))) 109 | {} 110 | assets)) 111 | 112 | (s/defn wrap-with-exporter 113 | "Wraps a Ring handler so that, periodically, assets identified in the configuration 114 | are checked for changes and copied out to the file system (as needed). 115 | 116 | The first checks and exports occur before delegating to the wrapped handler; this will allow 117 | a standard resource handler (say, ring.middleware.resource/wrap-resource) to operate. 118 | 119 | Subsequent checks happen at intervals on a secondary thread. 120 | The thread will shutdown once the Ring request pipeline is GC'ed. 121 | It requires an explicit call to System/gc to ensure that the pipeline is GC'ed 122 | (and the necessary object finalizer called). 123 | 124 | Note that asset exporting is largely intended for development purposes." 125 | [ring-handler 126 | {:keys [assets output-uri] :as configuration} :- ExportsConfiguration] 127 | (if (empty? assets) 128 | ring-handler 129 | (let [token (atom nil) 130 | asset-aliases (build-asset-aliases assets output-uri)] 131 | (fn [request] 132 | (when (nil? @token) 133 | (reset! token (start-exporter-thread (:twixt request) configuration))) 134 | 135 | (ring-handler (assoc-in request [:twixt :asset-aliases] asset-aliases)))))) 136 | -------------------------------------------------------------------------------- /src/io/aviso/twixt/fs_cache.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.fs-cache 2 | "Provide asset pipeline middleware that implements a file-system cache. 3 | The cache will persist between executions of the application; this is used in development to prevent 4 | duplicated, expensive work from being performed after a restart." 5 | (:import [java.util UUID] 6 | [java.io PushbackReader File Writer]) 7 | (:require 8 | [clojure.java.io :as io] 9 | [clojure 10 | [edn :as edn] 11 | [pprint :as pp]] 12 | [medley.core :as medley] 13 | [clojure.tools.logging :as l] 14 | [io.aviso.tracker :as t])) 15 | 16 | 17 | (defn- checksum-matches? 18 | [asset-resolver asset-path cached-checksum] 19 | (-> 20 | asset-path 21 | (asset-resolver nil) 22 | :checksum 23 | (= cached-checksum))) 24 | 25 | (defn- is-valid? 26 | [asset-resolver cached-asset] 27 | (and cached-asset 28 | (every? 29 | (fn [{:keys [asset-path checksum]}] 30 | (checksum-matches? asset-resolver asset-path checksum)) 31 | (-> cached-asset :dependencies vals)))) 32 | 33 | (defn- read-cached-asset-data 34 | [^File file] 35 | (if (.exists file) 36 | (with-open [reader (-> file io/reader PushbackReader.)] 37 | (edn/read reader)))) 38 | 39 | (defn- read-attachments 40 | [asset asset-cache-dir] 41 | (if (-> asset :attachments empty?) 42 | asset 43 | (update-in asset [:attachments] 44 | (fn [attachments] 45 | (medley/map-vals 46 | #(assoc % :content (io/file asset-cache-dir (:content %))) 47 | attachments))))) 48 | 49 | (def ^:private asset-file-name "asset.edn") 50 | (def ^:private content-file-name "content") 51 | 52 | (defn- read-cached-asset 53 | "Attempt to read the cached asset, if it exists. 54 | 55 | `asset-cache-dir` is the directory containing the two files (asset.edn and content)." 56 | [^File asset-cache-dir] 57 | (if (.exists asset-cache-dir) 58 | (t/track 59 | #(format "Reading from asset cache `%s'" asset-cache-dir) 60 | (some-> 61 | (io/file asset-cache-dir asset-file-name) 62 | read-cached-asset-data 63 | (assoc :content (io/file asset-cache-dir content-file-name)) 64 | (read-attachments asset-cache-dir))))) 65 | 66 | 67 | (defn- write-attachment 68 | "Writes the attachment's content to a file, returns the attachment with `:content` modified 69 | to be the simple name of the file written." 70 | [asset-cache-dir name attachment] 71 | (let [file-name (str (UUID/randomUUID) "-" name) 72 | content-file (io/file asset-cache-dir file-name)] 73 | (io/copy (:content attachment) content-file) 74 | (assoc attachment :content file-name))) 75 | 76 | (defn- write-attachments 77 | "Writes attachments as files in the cache dir, updating each's `:content` key into a simple file name." 78 | [asset asset-cache-dir] 79 | (if (-> asset :attachments empty?) 80 | asset 81 | (update-in asset [:attachments] 82 | (fn [attachments] 83 | (into {} 84 | (map (fn [[name attachment]] 85 | [name (write-attachment asset-cache-dir name attachment)]) 86 | attachments)))))) 87 | 88 | (defn- write-cached-asset 89 | [^File asset-cache-dir asset] 90 | (t/track 91 | #(format "Writing to asset cache `%s'" asset-cache-dir) 92 | (.mkdirs asset-cache-dir) 93 | (let [content-file (io/file asset-cache-dir content-file-name) 94 | asset-file (io/file asset-cache-dir asset-file-name)] 95 | ;; Write the content first 96 | (io/copy (:content asset) content-file) 97 | (with-open [^Writer writer (io/writer asset-file)] 98 | (.write writer (-> asset 99 | (write-attachments asset-cache-dir) 100 | ;; The name of the content file is (currently) a fixed value, 101 | ;; so we can just remove the key, rather than replace it with a file name. 102 | (dissoc :content) 103 | ;; Override *print-length* and *print-level* to ensure it is all written out. 104 | ^String (pp/write :length nil :level nil :stream nil))) 105 | (.write writer "\n"))))) 106 | 107 | (defn- delete-dir-and-contents 108 | [^File dir] 109 | (when (.exists dir) 110 | (t/track 111 | #(format "Deleting directory `%s'" dir) 112 | (doseq [file (.listFiles dir)] 113 | (t/track 114 | #(format "Deleting file `%s'" file) 115 | (io/delete-file file))) 116 | (io/delete-file dir)))) 117 | 118 | (defn wrap-with-filesystem-cache 119 | "Used to implement file-system caching of assets (this is typically only used in development, not production). 120 | File system caching improves startup and first-request time by avoiding the cost of recompling assets; instead 121 | the assets are stored on the file system. The asset checksums are used to see if the cache is still valid. 122 | 123 | Only assets that have truthy value for :compiled key will be file-system cached. This is set by the various 124 | compiler and transformers, such as the CoffeeScript to JavaScript transformer. 125 | 126 | Assets that are accessed for aggregation are not cached (the final aggregated asset will be cached). 127 | 128 | asset-handler 129 | : handler to be wrapped 130 | 131 | cache-dir-name 132 | : name of root folder to store cache in (from the Twixt options); the directory will be created as necessary" 133 | [asset-handler cache-dir-name] 134 | (let [cache-dir (io/file cache-dir-name "compiled")] 135 | (l/infof "Caching compiled assets to `%s'." cache-dir) 136 | (.mkdirs cache-dir) 137 | (fn [asset-path {:keys [asset-resolver for-aggregation] :as context}] 138 | (let [asset-cache-dir (io/file cache-dir asset-path) 139 | cached-asset (read-cached-asset asset-cache-dir)] 140 | (if (is-valid? asset-resolver cached-asset) 141 | cached-asset 142 | (do 143 | (delete-dir-and-contents asset-cache-dir) 144 | (let [asset (asset-handler asset-path context)] 145 | (if (and 146 | (:compiled asset) 147 | (not for-aggregation)) 148 | (write-cached-asset asset-cache-dir asset)) 149 | asset))))))) 150 | -------------------------------------------------------------------------------- /src/io/aviso/twixt/jade.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.jade 2 | "Provides asset pipeline middleware for compiling Jade templates to HTML using jade4j." 3 | (:require [clojure.java.io :as io] 4 | [io.aviso.twixt :as twixt] 5 | [io.aviso.tracker :as t] 6 | [io.aviso.twixt.utils :as utils] 7 | [medley.core :as medley]) 8 | (:import [de.neuland.jade4j JadeConfiguration] 9 | [de.neuland.jade4j.exceptions JadeException] 10 | [de.neuland.jade4j.template TemplateLoader])) 11 | 12 | (defn- add-missing-extension 13 | [^String name ext] 14 | (if (.endsWith name ext) 15 | name 16 | (str name ext))) 17 | 18 | (defn- create-template-loader [root-asset asset-resolver dependencies] 19 | (reify TemplateLoader 20 | ;; getLastModified is only needed for caching, and we disable Jade's caching 21 | ;; in favor of Twixt's. 22 | (getLastModified [_ name] -1) 23 | (getReader [_ name] 24 | (if (= name (:asset-path root-asset)) 25 | (-> root-asset :content io/reader) 26 | (let [full-name (add-missing-extension name ".jade")] 27 | (t/track 28 | #(format "Including Jade source from asset `%s'." full-name) 29 | ;; Use the asset-resolver, not the asset pipeline, because we need 30 | ;; the un-compiled version of the asset. 31 | (let [included (asset-resolver full-name nil)] 32 | (utils/nil-check included "Included asset does not exist.") 33 | (swap! dependencies utils/add-asset-as-dependency included) 34 | (-> included 35 | :content 36 | ;; We have to trust that Jade will close the reader. 37 | io/reader)))))))) 38 | 39 | (defn- wrap-asset-pipeline-with-dependency-tracker 40 | [asset-pipeline dependencies] 41 | (fn [asset-path context] 42 | (let [asset (asset-pipeline asset-path context)] 43 | (swap! dependencies utils/add-asset-as-dependency asset) 44 | asset))) 45 | 46 | (defn- create-shared-variables 47 | [asset {{:keys [helpers variables]} :jade :as context} dependencies] 48 | (let [context' (update-in context [:asset-pipeline] 49 | wrap-asset-pipeline-with-dependency-tracker dependencies)] 50 | (-> (medley/map-vals 51 | #(% asset context') 52 | (or helpers {})) 53 | (merge variables)))) 54 | 55 | (defn- ^JadeConfiguration create-configuration 56 | [pretty-print asset {:keys [asset-resolver] :as context} dependencies] 57 | (doto (JadeConfiguration.) 58 | (.setPrettyPrint pretty-print) 59 | (.setSharedVariables (create-shared-variables asset context dependencies)) 60 | (.setCaching false) 61 | (.setTemplateLoader (create-template-loader asset asset-resolver dependencies)))) 62 | 63 | (defn- jade-compiler [asset context] 64 | (let [name (:resource-path asset)] 65 | (t/timer 66 | #(format "Compiled `%s' to HTML in %.2f ms" name %) 67 | (t/track 68 | #(format "Compiling `%s' from Jade to HTML" name) 69 | (try 70 | ;; Seed the dependencies with the Jade source file. Any included 71 | ;; sources will be added to dependencies. 72 | (let [dependencies (atom {name (utils/extract-dependency asset)}) 73 | configuration (create-configuration (-> context :development-mode) asset context dependencies) 74 | template (.getTemplate configuration (:asset-path asset)) 75 | compiled-output (.renderTemplate configuration template {})] 76 | (utils/create-compiled-asset asset "text/html" compiled-output @dependencies)) 77 | (catch JadeException e 78 | (throw (RuntimeException. 79 | (format "Jade Compilation exception on line %d: %s" 80 | (.getLineNumber e) 81 | (or (.getMessage e) (-> e .getClass .getName))) 82 | e)))))))) 83 | 84 | (defn- complete-path 85 | "Computes the complete path for a partial path, relative to an existing asset. 86 | Alternately, if the path starts with a leading slash (an absolute path), the the leading path is stripped. 87 | 88 | The result is a complete asset path that can be passed to [[get-asset-uri]]." 89 | [asset ^String path] 90 | (if (.startsWith path "/") 91 | (.substring path 1) 92 | (utils/compute-relative-path (:asset-path asset) path))) 93 | 94 | (defprotocol TwixtHelper 95 | "A Jade4J helper object that is used to allow a template to resolve asset URIs." 96 | (uri 97 | [this path] 98 | "Used to obtain the URI for a given asset, identified by its asset path. 99 | The path may be relative to the currently compiling asset, or may be absolute (with a leading slash). 100 | 101 | Throws an exception if the asset it not found.") 102 | (uris 103 | [this paths] 104 | "Used to obtain multiple URIs for any number of assets, each identified by a path. 105 | 106 | paths is a seq of asset path strings (eitehr relative to the current asset, or absolute). 107 | 108 | *Added in 0.1.21*")) 109 | 110 | (defn- create-twixt-helper 111 | [asset context] 112 | (reify TwixtHelper 113 | (uri [_ path] 114 | (twixt/get-asset-uri context (complete-path asset path))) 115 | (uris [_ paths] 116 | (->> paths 117 | (map (partial complete-path asset)) 118 | (mapcat (partial twixt/get-asset-uris context)))))) 119 | 120 | (defn register-jade 121 | "Updates the Twixt options with support for compiling Jade into HTML. Pretty printing of the output HTML is enabled 122 | in development mode, but disabled in production (for efficiency)." 123 | [options] 124 | (-> options 125 | (assoc-in [:content-types "jade"] "text/jade") 126 | (assoc-in [:content-transformers "text/jade"] jade-compiler) 127 | (assoc-in [:twixt-template :jade :helpers "twixt"] create-twixt-helper))) -------------------------------------------------------------------------------- /src/io/aviso/twixt/js_minification.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.js-minification 2 | "Provides support for JavaScript minification using the Google Closure compiler." 3 | (:require [clojure.java.io :as io] 4 | [io.aviso.twixt.utils :as utils] 5 | [io.aviso.twixt.schemas :refer [AssetHandler]] 6 | [io.aviso.tracker :as t] 7 | [clojure.string :as str] 8 | [schema.core :as s]) 9 | (:import (com.google.javascript.jscomp CompilerOptions ClosureCodingConvention DiagnosticGroups CheckLevel 10 | CompilationLevel) 11 | [io.aviso.twixt.shims CompilerShim])) 12 | 13 | 14 | 15 | (defn- minimize-javascript-asset 16 | [{file-path :resource-path :as asset} compilation-level] 17 | (t/timer 18 | #(format "Minimized `%s' (%,d bytes) in %.2f ms" 19 | file-path (:size asset) %) 20 | (t/track 21 | #(format "Minimizing `%s' using Google Closure with compilation level %s" file-path compilation-level) 22 | (let [options ^CompilerOptions (doto (CompilerOptions.) 23 | (.setCodingConvention (ClosureCodingConvention.)) 24 | (.setOutputCharset "utf-8") 25 | (.setWarningLevel DiagnosticGroups/CHECK_VARIABLES CheckLevel/WARNING)) 26 | [failures output] (CompilerShim/runCompiler options 27 | compilation-level 28 | file-path 29 | (-> asset :content io/input-stream))] 30 | (if failures 31 | (throw (ex-info (str "JavaScript minimization failed: " 32 | (str/join "; " (seq failures))) 33 | asset)) 34 | (utils/create-compiled-asset asset "text/javascript" output nil)))))) 35 | 36 | (def ^:private optimization-levels {:simple CompilationLevel/SIMPLE_OPTIMIZATIONS 37 | :whitespace CompilationLevel/WHITESPACE_ONLY 38 | :advanced CompilationLevel/ADVANCED_OPTIMIZATIONS}) 39 | 40 | (s/defn wrap-with-javascript-minimizations :- AssetHandler 41 | "Identifies JavaScript assets and, if not aggregating, passes them through the Google Closure compiler." 42 | [asset-handler :- AssetHandler 43 | {:keys [development-mode js-optimizations]}] 44 | (let [js-optimizations' (if (= :default js-optimizations) 45 | (if development-mode :none :simple) 46 | js-optimizations) 47 | level (optimization-levels js-optimizations')] 48 | (if (nil? level) 49 | asset-handler 50 | (fn [asset-path {:keys [for-aggregation] :as context}] 51 | (let [{:keys [content-type] :as asset} (asset-handler asset-path context)] 52 | (if (and (= "text/javascript" content-type) 53 | (not for-aggregation)) 54 | (minimize-javascript-asset asset level) 55 | asset)))))) -------------------------------------------------------------------------------- /src/io/aviso/twixt/less.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.less 2 | "Provides asset pipeline middleware for compiling Less source files to CSS." 3 | (:require [clojure.string :as str] 4 | [io.aviso.tracker :as t] 5 | [io.aviso.twixt.utils :as utils]) 6 | (:import [com.github.sommeri.less4j LessSource LessSource$FileNotFound Less4jException LessCompiler$Problem LessCompiler LessCompiler$CompilationResult LessCompiler$Configuration LessCompiler$SourceMapConfiguration] 7 | [com.github.sommeri.less4j.core DefaultLessCompiler])) 8 | 9 | ;; Putting this logic inside the (proxy) call causes some really awful Clojure compiler problems. 10 | ;; This shim seems to defuse that. 11 | (defn- find-relative 12 | [asset-resolver asset relative-path context] 13 | (-> 14 | asset 15 | :asset-path 16 | (utils/compute-relative-path relative-path) 17 | (asset-resolver context))) 18 | 19 | (defn- ^LessSource create-less-source 20 | [asset-resolver dependencies asset context] 21 | ;; Whenever a LessSource is created, associated the asset as a dependency; this includes the primary source 22 | ;; and all imported sources. 23 | (swap! dependencies utils/add-asset-as-dependency asset) 24 | (proxy [LessSource] [] 25 | (relativeSource [filename] 26 | (if-let [rel (find-relative asset-resolver asset filename context)] 27 | (create-less-source asset-resolver dependencies rel context) 28 | (throw (new LessSource$FileNotFound)))) 29 | 30 | (getName [] (-> asset :asset-path utils/path->name)) 31 | 32 | (toString [] (:resource-path asset)) 33 | 34 | (getContent [] 35 | (-> 36 | asset 37 | :content 38 | utils/as-string 39 | (.replace "\r\n" "\n"))) 40 | 41 | (getBytes [] (:content asset)))) 42 | 43 | (defn- problem-to-string [^LessCompiler$Problem problem] 44 | (let [source (-> problem .getSource .toString) 45 | line (-> problem .getLine) 46 | character (-> problem .getCharacter) 47 | message (-> problem .getMessage)] 48 | (str source 49 | ":" line 50 | ":" character 51 | ": " message))) 52 | 53 | (defn- format-less-exception [^Less4jException e] 54 | (let [problems (->> e .getErrors (map problem-to-string))] 55 | (str 56 | "Less compilation " 57 | (if (= 1 (count problems)) "error" "errors") 58 | ":\n" 59 | (str/join "\n" problems)))) 60 | 61 | 62 | (defn- compile-less 63 | [^LessCompiler less-compiler asset {:keys [asset-resolver] :as context}] 64 | (let [path (:resource-path asset)] 65 | (t/timer 66 | #(format "Compiled `%s' to CSS in %.2f ms" path %) 67 | (t/track 68 | #(format "Compiling `%s' from Less to CSS" path) 69 | (try 70 | (let [dependencies (atom {}) 71 | root-source (create-less-source asset-resolver dependencies asset context) 72 | ^LessCompiler$Configuration options (LessCompiler$Configuration.) 73 | _ (doto ^LessCompiler$SourceMapConfiguration (.getSourceMapConfiguration options) 74 | (.setLinkSourceMap false) 75 | (.setIncludeSourcesContent true)) 76 | ;; A bit of work to trick the Less compiler into writing the right thing into the output CSS. 77 | ^LessCompiler$CompilationResult output (.compile less-compiler root-source options) 78 | complete-css (str 79 | (.getCss output) 80 | "\n/*# sourceMappingURL=" 81 | (utils/path->name path) 82 | "@source.map */\n") 83 | source-map (.getSourceMap output)] 84 | (-> 85 | asset 86 | (utils/create-compiled-asset "text/css" complete-css @dependencies) 87 | (utils/add-attachment "source.map" "application/json" (utils/as-bytes source-map)))) 88 | (catch Less4jException e 89 | (throw (RuntimeException. (format-less-exception e) e)))))))) 90 | 91 | (defn register-less 92 | "Updates the Twixt options with support for compiling Less into CSS." 93 | [options] 94 | (-> options 95 | (assoc-in [:content-types "less"] "text/less") 96 | (assoc-in [:content-transformers "text/less"] (partial compile-less (DefaultLessCompiler.))))) 97 | -------------------------------------------------------------------------------- /src/io/aviso/twixt/memory_cache.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.memory-cache 2 | "Provides asset pipeline middleware implementing an in-memory cache for assets. 3 | This cache is used to bypass the normal loading, compiling, and transforming steps." 4 | (:require [io.aviso.twixt.schemas :refer [Asset AssetHandler]] 5 | [io.aviso.toolchest.macros :refer [cond-let]] 6 | [schema.core :as s]) 7 | (:import [java.util.concurrent.locks ReentrantLock Lock])) 8 | 9 | (s/defn wrap-with-transforming-cache :- AssetHandler 10 | "A cache that operates in terms of a transformation of assets: the canonical example 11 | is GZip compression, where a certain assets can be compressed to form new assets. 12 | 13 | Assets that are obtained for aggregation are _never_ cached; it is assumed that they are 14 | intermediate results and only the final aggregated asset needs to be cached. 15 | 16 | This cache assumes that another cache is present that can quickly obtain the 17 | non-transformed version of the asset. 18 | 19 | asset-handler 20 | : The downstream asset handler, from which the untransformed asset is obtained. 21 | 22 | store-in-cache? 23 | : Function that is passed an Asset and determines if it can be transformed and stored in the cache. 24 | 25 | asset-tranformer 26 | : Function that is passed the untransformed Asset and returns the transformed Asset to be stored 27 | in the cache." 28 | {:added "0.1.20"} 29 | [asset-handler :- AssetHandler 30 | store-in-cache? :- (s/=> s/Bool Asset) 31 | asset-transformer :- (s/=> Asset Asset)] 32 | (let [cache (atom {})] 33 | (fn [asset-path {:keys [for-aggregation] :as context}] 34 | (cond-let 35 | ;; We assume this is cheap because of caching 36 | [prime-asset (asset-handler asset-path context)] 37 | 38 | for-aggregation 39 | prime-asset 40 | 41 | (nil? prime-asset) 42 | nil 43 | 44 | (not (store-in-cache? prime-asset)) 45 | prime-asset 46 | 47 | [{:keys [transformed prime-checksum]} (get cache asset-path)] 48 | 49 | (= prime-checksum (:checksum prime-asset)) 50 | transformed 51 | 52 | :else 53 | (let [transformed (asset-transformer prime-asset)] 54 | (swap! cache assoc asset-path {:transformed transformed 55 | :prime-checksum prime-checksum}) 56 | transformed))))) 57 | 58 | (defmacro ^:private with-lock 59 | [lock & body] 60 | `(let [^Lock lock# ~lock] 61 | (try 62 | (.lock lock#) 63 | ~@body 64 | (finally 65 | (.unlock lock#))))) 66 | 67 | (defn- update-cache 68 | [cache asset-handler asset-path context] 69 | (let [cached-asset (asset-handler asset-path context)] 70 | (swap! cache update-in [asset-path] 71 | assoc :cached-asset cached-asset 72 | :cached-at (System/currentTimeMillis)) 73 | 74 | cached-asset)) 75 | 76 | (s/defn wrap-with-memory-cache :- AssetHandler 77 | "Wraps another asset handler in a simple in-memory cache. 78 | 79 | This is useful for both compiled and raw assets, as it does two things. 80 | 81 | On first access to an asset, a cache entry is created. Subsequent accesses 82 | to the cache entry within the check interval will be served from the cache. 83 | 84 | After the check interval, the delegate asset handler is used to see if the asset has changed, 85 | replacing it in the cache." 86 | {:added "0.1.20"} 87 | [asset-handler :- AssetHandler 88 | check-interval-ms :- s/Num] 89 | (let [cache (atom {}) 90 | cache-lock (ReentrantLock.)] 91 | (fn [asset-path context] 92 | (trampoline 93 | (fn reentry-point [] 94 | (cond-let 95 | [now (System/currentTimeMillis) 96 | {:keys [cached-asset cached-at] :as cache-entry} (get @cache asset-path)] 97 | 98 | (and cached-asset 99 | (< now (+ cached-at check-interval-ms))) 100 | cached-asset 101 | 102 | (nil? cache-entry) 103 | (let [lock (ReentrantLock.)] 104 | (with-lock cache-lock 105 | ;; There can be a race condition where two threads are trying to first 106 | ;; access the same asset. This detects that; the first thread will have 107 | ;; added a cache entry with just the :lock key, then released the cache 108 | ;; lock while obtaining the asset. The second thread will reach this point, 109 | ;; see that there's something in the cache, and loop back 110 | ;; to fall through the normal path, and the per-asset lock. 111 | (if (contains? @cache asset-path) 112 | ;; This releases the cache-lock and falls through the normal 113 | ;; processing. Depending on the timing, either the asset will just be in the cache 114 | ;; and ready to go, or this thread will block while the other thread 115 | ;; is obtaining the asset. 116 | reentry-point 117 | (do 118 | ;; Do this now, with the cache-lock locked 119 | (swap! cache assoc-in [asset-path :lock] lock) 120 | ;; And do this via trampoline, after the cache-lock is unlocked. 121 | #(with-lock lock 122 | (update-cache cache asset-handler asset-path context)))))) 123 | 124 | :else 125 | (with-lock (:lock cache-entry) 126 | (update-cache cache asset-handler asset-path context)))))))) -------------------------------------------------------------------------------- /src/io/aviso/twixt/rhino.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.rhino 2 | "Code to execute Rhino for purposes such as compiling CoffeeScript to JavaScript." 3 | ;; Liberally borrowing from Dieter! 4 | (:import 5 | [org.mozilla.javascript Context Scriptable Function]) 6 | (:require 7 | [clojure.java.io :as io] 8 | [io.aviso.tracker :as t])) 9 | 10 | 11 | (defn- load-script [^Context context scope file] 12 | (with-open [content-reader (-> file io/resource io/reader)] 13 | (t/track 14 | #(str "Loading JavaScript from " file) 15 | (.evaluateReader context scope content-reader file 1 nil)))) 16 | 17 | (defn- invoke-function [^Context context ^Scriptable scope ^String function-name arguments] 18 | (let [^Function js-fn (.get scope function-name scope)] 19 | (.call js-fn context scope nil (into-array arguments)))) 20 | 21 | (defn invoke-javascript 22 | "Invokes a JavaScript function, returning the result. 23 | 24 | script-paths 25 | : JavaScript files to load, as seq of classpath resource paths 26 | 27 | javascript-fn-name 28 | : name of JavaScript function to execute 29 | 30 | arguments 31 | : additional arguments to pass to the function 32 | 33 | Returns the JavaScript result; typically this will be a JavaScript object, and the properties 34 | of it can be accessed via the methods of `java.util.Map`." 35 | [script-paths javascript-fn-name & arguments] 36 | (let [context (Context/enter)] 37 | (try 38 | ;; Apparently, CoffeeScript can blow away a 64K limit pretty easily. 39 | (.setOptimizationLevel context -1) 40 | 41 | (let [scope (.initStandardObjects context)] 42 | 43 | ;; Dieter maintains a pool of configured Context instances so that we don't 44 | ;; have to re-load the scripts on each compilation. That would be nice. 45 | (doseq [file script-paths] 46 | (load-script context scope file)) 47 | 48 | (invoke-function context scope javascript-fn-name arguments)) 49 | 50 | (finally (Context/exit))))) 51 | -------------------------------------------------------------------------------- /src/io/aviso/twixt/ring.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.ring 2 | "Support needed to use Twixt inside a Ring handler pipeline." 3 | (:require [clojure.java.io :as io] 4 | [io.aviso.tracker :as t] 5 | [io.aviso.twixt [asset :as asset]] 6 | [ring.util.response :as r]) 7 | (:import [java.util Calendar TimeZone])) 8 | 9 | (def ^:private far-future 10 | (let [cal (doto (Calendar/getInstance (TimeZone/getTimeZone "UTC")) 11 | (.add Calendar/YEAR 10))] 12 | (.getTime cal))) 13 | 14 | (defn- asset->ring-response 15 | [asset attachment-name] 16 | (let [content-source (if (some? attachment-name) 17 | (get-in asset [:attachments attachment-name]) 18 | asset)] 19 | ;; content-source may be nil if the URL includes an attachment but the attachment doesn't exist. 20 | (if content-source 21 | ;; First the standard stuff ... 22 | (cond-> 23 | (-> 24 | content-source 25 | :content 26 | io/input-stream 27 | r/response 28 | (r/header "Content-Length" (:size content-source)) 29 | ;; Because any change to the content will create a new checksum and a new URL, a change to the content 30 | ;; is really an entirely new resource. The current resource is therefore immutable and can have a far-future expires 31 | ;; header. 32 | (r/header "Expires" far-future) 33 | (r/content-type (:content-type content-source))) 34 | 35 | ;; The optional extras ... 36 | ;; :compressed will never be true for an attachment (that may change in the future). 37 | (:compressed content-source) (r/header "Content-Encoding" "gzip"))))) 38 | 39 | 40 | (defn- match? [^String path-prefix ^String path] 41 | (and 42 | (.startsWith path path-prefix) 43 | (not (or (= path path-prefix) 44 | (.endsWith path "/"))))) 45 | 46 | (defn- split-asset-path-and-attachment-name 47 | [^String path] 48 | (let [atx (.indexOf path "@")] 49 | (if (< atx 0) 50 | [path nil] 51 | [(.substring path 0 atx) 52 | (.substring path (inc atx))]))) 53 | 54 | (defn- parse-path 55 | "Parses the complete request path into a checksum, compressed-flag, asset path and (optional) attachment name." 56 | [^String path-prefix ^String path] 57 | (let [suffix (.substring path (.length path-prefix)) 58 | slashx (.indexOf suffix "/") 59 | full-checksum (.substring suffix 0 slashx) 60 | compressed? (.startsWith full-checksum "z") 61 | checksum (if compressed? (.substring full-checksum 1) full-checksum) 62 | [asset-path attachment-name] (-> suffix (.substring (inc slashx)) split-asset-path-and-attachment-name)] 63 | [checksum 64 | compressed? 65 | asset-path 66 | attachment-name])) 67 | 68 | (defn- asset->redirect-response 69 | [status path-prefix asset] 70 | {:status status 71 | :headers {"Location" (asset/asset->request-path path-prefix asset)} 72 | :body ""}) 73 | 74 | (def ^:private asset->301-response (partial asset->redirect-response 301)) 75 | 76 | (defn- create-asset-response 77 | [path-prefix requested-checksum asset attachment-name] 78 | (cond 79 | (nil? asset) nil 80 | (= requested-checksum (:checksum asset)) (asset->ring-response asset attachment-name) 81 | :else (asset->301-response path-prefix asset))) 82 | 83 | (defn twixt-handler 84 | "A Ring request handler that identifies requests targetted for Twixt assets. Returns a Ring response map 85 | if the request is for an existing asset, otherwise returns nil. 86 | 87 | The path may indicate an attachment to the asset (such as the source.map for a compiled JavaScript file). 88 | The attachment is indicated as a suffix: the `@` symbol and the name of the attachment. 89 | 90 | Asset URLs always include the intended asset's checksum; if the actual asset checksum does not match, then 91 | a 301 (moved permanently) response is sent with the correct asset URL." 92 | [request] 93 | (let [path-prefix (-> request :twixt :path-prefix) 94 | path (:uri request)] 95 | (when (match? path-prefix path) 96 | (t/track 97 | #(format "Handling asset request `%s'" path) 98 | (let [[requested-checksum compressed? asset-path attachment-name] (parse-path path-prefix path) 99 | context (:twixt request) 100 | ;; When actually servicing an asset request, we have to trust the data in the URL 101 | ;; that determines whether to server the normal or gzip'ed resource. 102 | context' (assoc context :gzip-enabled compressed?) 103 | asset-pipeline (:asset-pipeline context)] 104 | (create-asset-response path-prefix 105 | requested-checksum 106 | (asset-pipeline asset-path context') 107 | attachment-name)))))) 108 | 109 | ;;; It's really difficult to start with a path, convert it to a resource, and figure out if it is a folder 110 | ;;; or a file on the classpath. Instead, we just assume that anything that doesn't look like a path that 111 | ;;; ends with a file extension can be ignored. 112 | 113 | (defn- handle-asset-redirect 114 | [^String uri context] 115 | ;; This may be too specific, may need to find a better way to differentiate between a "folder" and a file. 116 | ;; This just checks to see if the path ends with something that looks like an extension. We just have to 117 | ;; assume that none of the folders on classpath look like that! This is also a bit restrictive; it assumes 118 | ;; the extension consists of word characters, or the dash. 119 | (if (re-find #"\.[\w-]+$" uri) 120 | (let [{:keys [asset-pipeline path-prefix]} context 121 | asset-path (.substring uri 1)] 122 | (if-let [asset (asset-pipeline asset-path context)] 123 | 124 | (asset->redirect-response 302 path-prefix asset))))) 125 | 126 | (defn wrap-with-asset-redirector 127 | "In some cases, it is not possible for the client to know what the full asset URI will be, such as when the 128 | URL is composed on the client (in which case, the asset checksum will not be known). 129 | The redirector accepts any request path that maps to an asset and returns a redirect to the asset's true URL. 130 | Non-matching requests are passed through to the provided handler. 131 | 132 | For a file under `META-INF/assets`, such as `META-INF/assets/myapp/icon.png`, the redirector will match 133 | the URI `/myapp/icon.png` and send a redirect to `/assets/123abc/myapp/icon.png`. 134 | 135 | This middleware is __not__ applied by default." 136 | [handler] 137 | (fn [{uri :uri 138 | context :twixt 139 | :as request}] 140 | (or 141 | (handle-asset-redirect uri context) 142 | (handler request)))) 143 | 144 | (defn wrap-with-twixt 145 | "Invokes the twixt-handler and delegates to the provided Ring handler if twixt-handler returns nil. 146 | 147 | This assumes that the resulting handler will then be wrapped with the twixt setup. 148 | 149 | In most cases, you will want to use the `wrap-with-twixt` function in the `io.aviso.twixt.startup` namespace." 150 | [handler] 151 | (fn [request] 152 | (or (twixt-handler request) 153 | (handler request)))) 154 | 155 | (defn wrap-with-twixt-setup 156 | "Wraps a Ring handler with another Ring handler that provides the `:twixt` key in the request object. 157 | 158 | The `:twixt` key is the default asset pipeline context, which is needed by [[get-asset-uri]] in order to resolve asset paths 159 | to an actual asset. 160 | It also contains the keys `:asset-pipeline` (the pipeline used to resolve assets), 161 | `:stack-frame-filter` (which is used by the HTML exception report) and `:development-mode`. 162 | 163 | This provides the information needed by the actual Twixt handler, as well as anything else downstream that needs to 164 | generate Twixt asset URIs." 165 | [handler twixt-options asset-pipeline] 166 | (let [context (-> twixt-options 167 | ;; Pass down only what is needed to generate asset URIs, or to produce the HTML exception report. 168 | (select-keys [:path-prefix :stack-frame-filter :development-mode]) 169 | (assoc :asset-pipeline asset-pipeline) 170 | (merge (:twixt-template twixt-options)))] 171 | (fn [request] 172 | (handler (assoc request :twixt context))))) 173 | -------------------------------------------------------------------------------- /src/io/aviso/twixt/schemas.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.schemas 2 | "Defines schemas for the main types and functions." 3 | {:added "0.1.17"} 4 | (:require [schema.core :as s])) 5 | 6 | (def Content 7 | "Defines the content for an Asset; this is any value compatible with clojure.java.io." 8 | s/Any) 9 | 10 | (def AssetPath 11 | "Path of the asset under the root folder (which is typically `/META-INF/assets/`)." 12 | s/Str) 13 | 14 | (def ResourcePath 15 | "Full path of the underlying resource (on the classpath)." 16 | s/Str) 17 | 18 | (def ContentType 19 | "The MIME type of the content, as determined form the path's extension." 20 | s/Str) 21 | 22 | (def Size 23 | "Size of the content, in bytes." 24 | s/Num) 25 | 26 | (def Checksum 27 | "Computed Adler32 checksum of the content, as a string of hex characters." 28 | s/Str) 29 | 30 | (def ModifiedAt 31 | "Instant at which an asset was last modified; this may not be accurate for assets 32 | stored within a JAR file, but will be adequate for determining if a file's content 33 | has changed." 34 | s/Inst) 35 | 36 | (def AttachmentName 37 | "A unique logical name that represents an attachment to an asset. 38 | Attachments are a limited form of an Asset typically generated as a side-effect 39 | of creating/compiling the asset; the canonical example is a JavaScript source map." 40 | s/Str) 41 | 42 | (s/defschema Dependency 43 | {:asset-path AssetPath 44 | :checksum Checksum 45 | :modified-at ModifiedAt}) 46 | 47 | (s/defschema DependencyMap 48 | {ResourcePath Dependency}) 49 | 50 | (s/defschema Attachment 51 | {:content Content 52 | :size Size 53 | :content-type ContentType}) 54 | 55 | (s/defschema AttachmentMap 56 | {AttachmentName Attachment}) 57 | 58 | (s/defschema Asset 59 | "A server-side resource that may be exposed to a client (such as a web browser). 60 | An Asset may be transformed from raw content, for example to transpile a language 61 | to JavaScript. Other transformations include compression." 62 | {:asset-path AssetPath 63 | :resource-path ResourcePath 64 | :content-type ContentType 65 | :size Size 66 | :checksum Checksum 67 | :modified-at ModifiedAt 68 | :content Content 69 | (s/optional-key :compiled) s/Bool 70 | (s/optional-key :aggregate-asset-paths) [AssetPath] 71 | :dependencies DependencyMap 72 | (s/optional-key :attachments) AttachmentMap 73 | ;; Various plugins and extensions will add their own keys, so we need to not be picky: 74 | s/Any s/Any 75 | }) 76 | 77 | (s/defschema TwixtContext 78 | "Defines the minimal values provided in the Twixt context (the :twixt key of the Ring 79 | request map)." 80 | ;; :asset-pipeline will always be present when requests are processing; there's 81 | ;; just a bit of chicken-or-the-egg at startup time. 82 | {(s/optional-key :asset-pipeline) s/Any ; should be AssetHandler 83 | :path-prefix s/Str 84 | :development-mode s/Bool 85 | (s/optional-key :gzip-enabled) (s/maybe s/Bool) ; often set explicitly to false for aggregation 86 | ;; Other plugins are likely to add thier own data to the context 87 | ;; (via the :twixt-template key of the Twixt options). 88 | s/Any s/Any}) 89 | 90 | (s/defschema AssetHandler 91 | "An asset handler is passed as asset path and the Twixt context and, maybe, returns 92 | an Asset." 93 | (s/=> (s/maybe Asset) AssetPath TwixtContext)) 94 | 95 | (def AssetURI 96 | "A URI that allows an external client to access the client content. AssetURIs typically 97 | include the checksum in the URI path, forming an immutable web resource." 98 | s/Str) 99 | 100 | (s/defschema AssetExport 101 | "Either just an asset path (a String), or two strings: an Asset to export, and an alias 102 | to export it as." 103 | (s/either AssetPath 104 | [(s/one AssetPath 'asset-path) (s/one AssetPath 'output-alias)])) 105 | 106 | (s/defschema ExportsConfiguration 107 | "Defines the details of how assets are exported to the file system. 108 | 109 | :interval-ms 110 | : The time between checks for performing exports. 111 | : Checks only occur during requests, so an external ping process may be needed to keep checks running. 112 | 113 | :output-dir 114 | : The root directory to output files to. The directory will be created as necessary. 115 | 116 | :assets 117 | : The assets to export. 118 | : Keys are the assets to export." 119 | {:interval-ms s/Num 120 | :output-dir s/Str 121 | :output-uri s/Str 122 | :assets [AssetExport]}) -------------------------------------------------------------------------------- /src/io/aviso/twixt/stacks.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.stacks 2 | "Stacks are a way of combining several related files together to form a single aggregate virtual file. 3 | In development, the individual files are used, but in production they are aggregated together so that 4 | the entire stack can be obtained in a single request." 5 | {:since "0.1.13"} 6 | (:require [io.aviso.tracker :as t] 7 | [io.aviso.twixt.utils :as utils] 8 | [clojure.java.io :as io] 9 | [clojure.edn :as edn]) 10 | (:import (java.io PushbackReader ByteArrayOutputStream))) 11 | 12 | (def stack-mime-type 13 | "Defines a MIME type for a Twixt stack, a kind of aggregate resource that combines 14 | multiple other resources." 15 | "application/vnd.twixt-stack+edn") 16 | 17 | (defn- read-stack 18 | [asset] 19 | (-> asset :content io/reader PushbackReader. edn/read)) 20 | 21 | (defn- update-aggregate-asset-paths 22 | [asset-paths component-asset] 23 | (if-let [aggregate-paths (-> component-asset :aggregate-asset-paths seq)] 24 | (concat asset-paths aggregate-paths) 25 | (conj asset-paths (:asset-path component-asset)))) 26 | 27 | (defn- include-component 28 | [{:keys [asset-pipeline] :as context} {:keys [asset-path] :as asset} component-path content-stream] 29 | ;; This may only be correct for relative, not absolute, paths: 30 | (let [complete-path (utils/compute-relative-path asset-path component-path) 31 | component-asset (asset-pipeline complete-path context)] 32 | 33 | (if-not (some? component-asset) 34 | (throw (ex-info (format "Could not locate resource `%s' (a component of `%s')." 35 | complete-path 36 | asset-path) 37 | context))) 38 | 39 | (io/copy (:content component-asset) content-stream) 40 | 41 | ;; Now add the component asset as a dependency by itself, or 42 | ;; add the component asset's dependencies 43 | 44 | (-> asset 45 | (update-in [:dependencies] utils/add-asset-as-dependency component-asset) 46 | (update-in [:aggregate-asset-paths] (fnil update-aggregate-asset-paths []) component-asset)))) 47 | 48 | (defn- aggregate-stack 49 | [{:keys [resource-path] :as asset} context] 50 | (t/track 51 | #(format "Reading stack `%s' and aggregating contents." resource-path) 52 | ;; Make sure that the aggregated asset is not compressed 53 | (let [context' (assoc context :for-aggregation true) 54 | stack (read-stack asset)] 55 | 56 | (assert (-> stack :content-type some?)) 57 | (assert (-> stack :components empty? not)) 58 | 59 | (with-open [content-stream (ByteArrayOutputStream. 5000)] 60 | (-> (reduce (fn [asset component-path] (include-component context' asset component-path content-stream)) asset (:components stack)) 61 | (assoc-in [:dependencies resource-path] (utils/extract-dependency asset)) 62 | (utils/replace-asset-content (:content-type stack) (.toByteArray content-stream)) 63 | (assoc :compiled true)))))) 64 | 65 | (defn register-stacks 66 | "Updates the Twixt options with support for stacks." 67 | [options] 68 | (-> options 69 | (assoc-in [:content-types "stack"] stack-mime-type) 70 | (assoc-in [:content-transformers stack-mime-type] aggregate-stack))) -------------------------------------------------------------------------------- /src/io/aviso/twixt/startup.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.startup 2 | "Breaks out the default logic for initializing Twixt's handlers and middleware." 3 | (:require 4 | [io.aviso.twixt :as t] 5 | [io.aviso.twixt 6 | [coffee-script :as cs] 7 | [compress :as compress] 8 | [exceptions :as te] 9 | [export :as export] 10 | [jade :as jade] 11 | [less :as less] 12 | [ring :as ring] 13 | [stacks :as stacks]])) 14 | 15 | (defn wrap-with-twixt 16 | "The default way to setup Twixt, with exception reporting. 17 | This (currently) enables support for CoffeeScript, Less, and Jade, and Stacks. 18 | 19 | The provided Ring request handler is wrapped in the following stack (outermost to innermost): 20 | 21 | - twixt setup (adds `:twixt` key to the request) 22 | - exception reporting 23 | - compression analyzer (does the client support GZip encoding?) 24 | - asset export logic (exports certain assets to file system when changed 25 | - asset request handling 26 | - the provided handler 27 | 28 | With just a handler, uses the default Twixt options and production mode. 29 | 30 | The two argument version is used to set development-mode, but use default options. 31 | 32 | Otherwise, provide the handler, alternate options and true or false for development mode. 33 | The alternate options are merged with defaults and override them." 34 | ([handler] 35 | (wrap-with-twixt handler false)) 36 | ([handler development-mode] 37 | (wrap-with-twixt handler t/default-options development-mode)) 38 | ([handler opts development-mode] 39 | (let [twixt-options (-> (merge t/default-options opts) 40 | (assoc :development-mode development-mode) 41 | te/register-exception-reporting 42 | cs/register-coffee-script 43 | jade/register-jade 44 | less/register-less 45 | stacks/register-stacks) 46 | asset-pipeline (t/default-asset-pipeline twixt-options)] 47 | (-> 48 | handler 49 | ring/wrap-with-twixt 50 | (export/wrap-with-exporter (:exports twixt-options)) 51 | te/wrap-with-exception-reporting 52 | compress/wrap-with-compression-analyzer 53 | (ring/wrap-with-twixt-setup twixt-options asset-pipeline))))) -------------------------------------------------------------------------------- /src/io/aviso/twixt/utils.clj: -------------------------------------------------------------------------------- 1 | (ns io.aviso.twixt.utils 2 | "Some re-usable utilities. 3 | 4 | Many of these are useful when creating new compilers or translators for Twixt." 5 | (:require [clojure.java.io :as io] 6 | [clojure.string :as str] 7 | [schema.core :as s] 8 | [io.aviso.twixt.schemas :refer :all]) 9 | (:import [java.io CharArrayWriter ByteArrayOutputStream File] 10 | [java.util.zip Adler32] 11 | [java.net URL URI] 12 | [java.util Date])) 13 | 14 | (defn ^String as-string 15 | "Converts a source (compatible with clojure.java.io/IOFactory) into a String using the provided encoding. 16 | 17 | The source is typically a byte array, or a File. 18 | 19 | The default charset is UTF-8." 20 | ([source] 21 | (as-string source "UTF-8")) 22 | ([source charset] 23 | (with-open [reader (io/reader source :encoding charset) 24 | writer (CharArrayWriter. 1000)] 25 | (io/copy reader writer) 26 | (.toString writer)))) 27 | 28 | (defn as-bytes [^String string] 29 | "Converts a string to a byte array. The string should be UTF-8 encoded." 30 | (.getBytes string "UTF-8")) 31 | 32 | (s/defn compute-checksum :- Checksum 33 | "Returns a hex string of the Adler32 checksum of the content." 34 | [^bytes content] 35 | (-> 36 | (doto (Adler32.) 37 | (.update content)) 38 | .getValue 39 | Long/toHexString)) 40 | 41 | (s/defn replace-asset-content :- Asset 42 | "Modifies an Asset new content. 43 | This updates the :size and :checksum properties as well." 44 | [asset :- Asset 45 | content-type :- ContentType 46 | ^bytes content-bytes] 47 | (assoc asset 48 | :content-type content-type 49 | :content content-bytes 50 | :size (alength content-bytes) 51 | :checksum (compute-checksum content-bytes))) 52 | 53 | (s/defn extract-dependency :- Dependency 54 | "Extracts from the asset the keys needed to track dependencies (used by caching logic)." 55 | [asset :- Asset] 56 | (select-keys asset [:checksum :modified-at :asset-path])) 57 | 58 | (s/defn add-asset-as-dependency :- DependencyMap 59 | "Adds the dependencies of the Asset to a dependency map." 60 | [dependencies :- DependencyMap 61 | asset :- Asset] 62 | (merge dependencies (:dependencies asset))) 63 | 64 | (s/defn create-compiled-asset :- Asset 65 | "Used to transform an Asset after it has been compiled from one form to another. 66 | Dependencies is a map of resource path to source asset details, used to check cache validity. 67 | 68 | The source asset's dependencies are merged into any provided dependencies to form the :dependencies entry of the output Asset." 69 | [source-asset :- Asset 70 | content-type :- s/Str 71 | content :- s/Str 72 | dependencies :- (s/maybe DependencyMap)] 73 | (let [merged-dependencies (add-asset-as-dependency (or dependencies {}) source-asset)] 74 | (-> 75 | source-asset 76 | (replace-asset-content content-type (as-bytes content)) 77 | (assoc :compiled true :dependencies merged-dependencies)))) 78 | 79 | (s/defn add-attachment :- Asset 80 | "Adds an attachment to an asset." 81 | {:since "0.1.13"} 82 | [asset :- Asset 83 | name :- AttachmentName 84 | content-type :- ContentType 85 | ^bytes content] 86 | (assoc-in asset [:attachments name] {:content-type content-type 87 | :content content 88 | :size (alength content)})) 89 | 90 | (defn read-content 91 | "Reads the content of a provided source (compatible with `clojure.java.io/input-stream`) as a byte array 92 | 93 | The content is usually a URI or URL." 94 | [source] 95 | (assert source "Unable to read content from nil.") 96 | (with-open [bos (ByteArrayOutputStream.) 97 | in (io/input-stream source)] 98 | (io/copy in bos) 99 | (.toByteArray bos))) 100 | 101 | (defn compute-relative-path 102 | [^String start ^String relative] 103 | ;; Convert the start path into a stack of just the folders 104 | (loop [path-terms (-> (.split start "/") reverse rest) 105 | terms (-> relative (.split "/"))] 106 | (let [[term & remaining] terms] 107 | (cond 108 | (empty? terms) (->> path-terms reverse (str/join "/")) 109 | (or (= term ".") (= term "")) (recur path-terms remaining) 110 | (= term "..") (if (empty? path-terms) 111 | ;; You could rewrite this with reduce, but then generating this exception would be more difficult: 112 | (throw (IllegalArgumentException. (format "Relative path `%s' for `%s' would go above root." relative start))) 113 | (recur (rest path-terms) remaining)) 114 | :else (recur (cons term path-terms) remaining))))) 115 | 116 | (defn- url-to-file 117 | [^URL url] 118 | (-> url .toURI File.)) 119 | 120 | (defn- jar-to-file 121 | "For a URL that points to a file inside a jar, this returns the JAR file itself." 122 | [^URL url] 123 | (-> url 124 | .getPath 125 | (str/split #"!") 126 | first 127 | URI. 128 | File.)) 129 | 130 | ;; Not the same as io/file! 131 | (defn- ^File as-file 132 | "Locates a file from which a last modified date can be extracted." 133 | [^URL url] 134 | (cond 135 | (= "jar" (.getProtocol url)) (jar-to-file url) 136 | :else (url-to-file url))) 137 | 138 | (s/defn modified-at :- Date 139 | "Extracts a last-modified Date" 140 | [url :- URL] 141 | (if-let [^long time-modified (some-> url as-file .lastModified)] 142 | (Date. time-modified))) 143 | 144 | (defn nil-check 145 | {:no-doc true} 146 | [value message] 147 | (or 148 | value 149 | (throw (NullPointerException. message)))) 150 | 151 | (defn path->name 152 | "Converts a path to just the file name, the last term in the path." 153 | [^String path] 154 | (let [slashx (.lastIndexOf path "/")] 155 | (if (< slashx 0) 156 | path 157 | (.substring path (inc slashx))))) 158 | 159 | --------------------------------------------------------------------------------