├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── Rakefile ├── Readme.mkdn ├── lib └── rack │ └── coffee.rb ├── rack-coffee.gemspec └── test ├── javascripts ├── cache_compile.coffee ├── static.js └── test.coffee ├── other_javascripts └── test.coffee └── rack_coffee_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.1.1 4 | - 2.1.0 5 | - 2.0.0 6 | - 1.9.3 7 | - ree 8 | - rbx-2.2.6 9 | - 1.8.7 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "rack" 4 | gem "coffee-script" 5 | 6 | group :test do 7 | gem "rake" 8 | 9 | # required for TravisCI: http://docs.travis-ci.com/user/languages/ruby/#Rubinius 10 | platforms :rbx do 11 | gem "racc" 12 | gem "rubysl", "~>2.0" 13 | gem "psych" 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | coffee-script (2.2.0) 5 | coffee-script-source 6 | execjs 7 | coffee-script-source (1.2.0) 8 | execjs (1.3.0) 9 | multi_json (~> 1.0) 10 | ffi2-generators (0.1.1) 11 | multi_json (1.1.0) 12 | psych (2.0.5) 13 | racc (1.4.11) 14 | rack (1.4.1) 15 | rake (10.1.0) 16 | rubysl (2.0.15) 17 | rubysl-abbrev (~> 2.0) 18 | rubysl-base64 (~> 2.0) 19 | rubysl-benchmark (~> 2.0) 20 | rubysl-bigdecimal (~> 2.0) 21 | rubysl-cgi (~> 2.0) 22 | rubysl-cgi-session (~> 2.0) 23 | rubysl-cmath (~> 2.0) 24 | rubysl-complex (~> 2.0) 25 | rubysl-continuation (~> 2.0) 26 | rubysl-coverage (~> 2.0) 27 | rubysl-csv (~> 2.0) 28 | rubysl-curses (~> 2.0) 29 | rubysl-date (~> 2.0) 30 | rubysl-delegate (~> 2.0) 31 | rubysl-digest (~> 2.0) 32 | rubysl-drb (~> 2.0) 33 | rubysl-e2mmap (~> 2.0) 34 | rubysl-english (~> 2.0) 35 | rubysl-enumerator (~> 2.0) 36 | rubysl-erb (~> 2.0) 37 | rubysl-etc (~> 2.0) 38 | rubysl-expect (~> 2.0) 39 | rubysl-fcntl (~> 2.0) 40 | rubysl-fiber (~> 2.0) 41 | rubysl-fileutils (~> 2.0) 42 | rubysl-find (~> 2.0) 43 | rubysl-forwardable (~> 2.0) 44 | rubysl-getoptlong (~> 2.0) 45 | rubysl-gserver (~> 2.0) 46 | rubysl-io-console (~> 2.0) 47 | rubysl-io-nonblock (~> 2.0) 48 | rubysl-io-wait (~> 2.0) 49 | rubysl-ipaddr (~> 2.0) 50 | rubysl-irb (~> 2.0) 51 | rubysl-logger (~> 2.0) 52 | rubysl-mathn (~> 2.0) 53 | rubysl-matrix (~> 2.0) 54 | rubysl-mkmf (~> 2.0) 55 | rubysl-monitor (~> 2.0) 56 | rubysl-mutex_m (~> 2.0) 57 | rubysl-net-ftp (~> 2.0) 58 | rubysl-net-http (~> 2.0) 59 | rubysl-net-imap (~> 2.0) 60 | rubysl-net-pop (~> 2.0) 61 | rubysl-net-protocol (~> 2.0) 62 | rubysl-net-smtp (~> 2.0) 63 | rubysl-net-telnet (~> 2.0) 64 | rubysl-nkf (~> 2.0) 65 | rubysl-observer (~> 2.0) 66 | rubysl-open-uri (~> 2.0) 67 | rubysl-open3 (~> 2.0) 68 | rubysl-openssl (~> 2.0) 69 | rubysl-optparse (~> 2.0) 70 | rubysl-ostruct (~> 2.0) 71 | rubysl-pathname (~> 2.0) 72 | rubysl-prettyprint (~> 2.0) 73 | rubysl-prime (~> 2.0) 74 | rubysl-profile (~> 2.0) 75 | rubysl-profiler (~> 2.0) 76 | rubysl-pstore (~> 2.0) 77 | rubysl-pty (~> 2.0) 78 | rubysl-rational (~> 2.0) 79 | rubysl-readline (~> 2.0) 80 | rubysl-resolv (~> 2.0) 81 | rubysl-rexml (~> 2.0) 82 | rubysl-rinda (~> 2.0) 83 | rubysl-rss (~> 2.0) 84 | rubysl-scanf (~> 2.0) 85 | rubysl-securerandom (~> 2.0) 86 | rubysl-set (~> 2.0) 87 | rubysl-shellwords (~> 2.0) 88 | rubysl-singleton (~> 2.0) 89 | rubysl-socket (~> 2.0) 90 | rubysl-stringio (~> 2.0) 91 | rubysl-strscan (~> 2.0) 92 | rubysl-sync (~> 2.0) 93 | rubysl-syslog (~> 2.0) 94 | rubysl-tempfile (~> 2.0) 95 | rubysl-thread (~> 2.0) 96 | rubysl-thwait (~> 2.0) 97 | rubysl-time (~> 2.0) 98 | rubysl-timeout (~> 2.0) 99 | rubysl-tmpdir (~> 2.0) 100 | rubysl-tsort (~> 2.0) 101 | rubysl-un (~> 2.0) 102 | rubysl-uri (~> 2.0) 103 | rubysl-weakref (~> 2.0) 104 | rubysl-webrick (~> 2.0) 105 | rubysl-xmlrpc (~> 2.0) 106 | rubysl-yaml (~> 2.0) 107 | rubysl-zlib (~> 2.0) 108 | rubysl-abbrev (2.0.4) 109 | rubysl-base64 (2.0.0) 110 | rubysl-benchmark (2.0.1) 111 | rubysl-bigdecimal (2.0.2) 112 | rubysl-cgi (2.0.1) 113 | rubysl-cgi-session (2.0.1) 114 | rubysl-cmath (2.0.0) 115 | rubysl-complex (2.0.0) 116 | rubysl-continuation (2.0.0) 117 | rubysl-coverage (2.0.3) 118 | rubysl-csv (2.0.2) 119 | rubysl-english (~> 2.0) 120 | rubysl-curses (2.0.1) 121 | rubysl-date (2.0.6) 122 | rubysl-delegate (2.0.1) 123 | rubysl-digest (2.0.3) 124 | rubysl-drb (2.0.1) 125 | rubysl-e2mmap (2.0.0) 126 | rubysl-english (2.0.0) 127 | rubysl-enumerator (2.0.0) 128 | rubysl-erb (2.0.1) 129 | rubysl-etc (2.0.3) 130 | ffi2-generators (~> 0.1) 131 | rubysl-expect (2.0.0) 132 | rubysl-fcntl (2.0.4) 133 | ffi2-generators (~> 0.1) 134 | rubysl-fiber (2.0.0) 135 | rubysl-fileutils (2.0.3) 136 | rubysl-find (2.0.1) 137 | rubysl-forwardable (2.0.1) 138 | rubysl-getoptlong (2.0.0) 139 | rubysl-gserver (2.0.0) 140 | rubysl-socket (~> 2.0) 141 | rubysl-thread (~> 2.0) 142 | rubysl-io-console (2.0.0) 143 | rubysl-io-nonblock (2.0.0) 144 | rubysl-io-wait (2.0.0) 145 | rubysl-ipaddr (2.0.0) 146 | rubysl-irb (2.0.4) 147 | rubysl-e2mmap (~> 2.0) 148 | rubysl-mathn (~> 2.0) 149 | rubysl-readline (~> 2.0) 150 | rubysl-thread (~> 2.0) 151 | rubysl-logger (2.0.0) 152 | rubysl-mathn (2.0.0) 153 | rubysl-matrix (2.1.0) 154 | rubysl-e2mmap (~> 2.0) 155 | rubysl-mkmf (2.0.1) 156 | rubysl-fileutils (~> 2.0) 157 | rubysl-shellwords (~> 2.0) 158 | rubysl-monitor (2.0.0) 159 | rubysl-mutex_m (2.0.0) 160 | rubysl-net-ftp (2.0.1) 161 | rubysl-net-http (2.0.4) 162 | rubysl-cgi (~> 2.0) 163 | rubysl-erb (~> 2.0) 164 | rubysl-singleton (~> 2.0) 165 | rubysl-net-imap (2.0.1) 166 | rubysl-net-pop (2.0.1) 167 | rubysl-net-protocol (2.0.1) 168 | rubysl-net-smtp (2.0.1) 169 | rubysl-net-telnet (2.0.0) 170 | rubysl-nkf (2.0.1) 171 | rubysl-observer (2.0.0) 172 | rubysl-open-uri (2.0.0) 173 | rubysl-open3 (2.0.0) 174 | rubysl-openssl (2.1.0) 175 | rubysl-optparse (2.0.1) 176 | rubysl-shellwords (~> 2.0) 177 | rubysl-ostruct (2.0.4) 178 | rubysl-pathname (2.0.0) 179 | rubysl-prettyprint (2.0.3) 180 | rubysl-prime (2.0.1) 181 | rubysl-profile (2.0.0) 182 | rubysl-profiler (2.0.1) 183 | rubysl-pstore (2.0.0) 184 | rubysl-pty (2.0.2) 185 | rubysl-rational (2.0.1) 186 | rubysl-readline (2.0.2) 187 | rubysl-resolv (2.1.0) 188 | rubysl-rexml (2.0.2) 189 | rubysl-rinda (2.0.1) 190 | rubysl-rss (2.0.0) 191 | rubysl-scanf (2.0.0) 192 | rubysl-securerandom (2.0.0) 193 | rubysl-set (2.0.1) 194 | rubysl-shellwords (2.0.0) 195 | rubysl-singleton (2.0.0) 196 | rubysl-socket (2.0.1) 197 | rubysl-stringio (2.0.0) 198 | rubysl-strscan (2.0.0) 199 | rubysl-sync (2.0.0) 200 | rubysl-syslog (2.0.1) 201 | ffi2-generators (~> 0.1) 202 | rubysl-tempfile (2.0.1) 203 | rubysl-thread (2.0.2) 204 | rubysl-thwait (2.0.0) 205 | rubysl-time (2.0.3) 206 | rubysl-timeout (2.0.0) 207 | rubysl-tmpdir (2.0.1) 208 | rubysl-tsort (2.0.1) 209 | rubysl-un (2.0.0) 210 | rubysl-fileutils (~> 2.0) 211 | rubysl-optparse (~> 2.0) 212 | rubysl-uri (2.0.0) 213 | rubysl-weakref (2.0.0) 214 | rubysl-webrick (2.0.0) 215 | rubysl-xmlrpc (2.0.0) 216 | rubysl-yaml (2.0.4) 217 | rubysl-zlib (2.0.1) 218 | 219 | PLATFORMS 220 | ruby 221 | 222 | DEPENDENCIES 223 | coffee-script 224 | psych 225 | racc 226 | rack 227 | rake 228 | rubysl (~> 2.0) 229 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | 3 | desc "Run all the tests" 4 | task :default => [:test] 5 | 6 | Rake::TestTask.new(:test) do |t| 7 | t.libs << 'test' 8 | t.pattern = 'test/*_test.rb' 9 | t.verbose = true 10 | t.warning = false 11 | end 12 | 13 | begin 14 | require 'rubygems' 15 | rescue LoadError 16 | # Too bad. 17 | else 18 | desc "build the gemspec file" 19 | task "rack-coffee.gemspec" do 20 | spec = Gem::Specification.new do |s| 21 | s.name = "rack-coffee" 22 | s.version = "1.0.3" 23 | s.license = "MIT" 24 | s.platform = Gem::Platform::RUBY 25 | s.summary = "serve up coffeescript from rack middleware" 26 | 27 | s.description = <<-EOF.gsub(/\s+/,' ').strip 28 | Rack Middlware for compiling and serving .coffee files using 29 | coffee-script; "/javascripts/app.js" compiles and serves 30 | "/javascipts/app.coffee". 31 | EOF 32 | 33 | s.files = `git ls-files`.split("\n") 34 | s.require_path = 'lib' 35 | s.has_rdoc = false 36 | s.test_files = Dir['test/*_test.rb'] 37 | 38 | s.authors = ['Matthew Lyon', 'Brian Mitchell'] 39 | s.email = 'matthew@lyonheart.us' 40 | s.homepage = 'http://github.com/mattly/rack-coffee' 41 | s.rubyforge_project = 'rack-coffee' 42 | 43 | s.add_dependency 'rack' 44 | s.add_dependency 'coffee-script' 45 | end 46 | 47 | File.open("rack-coffee.gemspec", "w") { |f| f << spec.to_ruby } 48 | end 49 | 50 | desc "build the gem" 51 | task :gem => ["rack-coffee.gemspec"] do 52 | sh "gem build rack-coffee.gemspec" 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /Readme.mkdn: -------------------------------------------------------------------------------- 1 | # rack-coffee 2 | 3 | Simple rack middleware for serving up [CoffeeScript][coffee] files as compiled 4 | javascript. 5 | 6 | [![Build Status](https://travis-ci.org/mattly/rack-coffee.png?branch=master)](https://travis-ci.org/mattly/rack-coffee) 7 | 8 | ## Usage 9 | 10 | The options behave similarly to Rack::Static: 11 | 12 | require 'rack/coffee' 13 | use Rack::Coffee, 14 | :root => '/path/to/directory/above/url', 15 | :urls => '/javascipts' 16 | 17 | For rails, presuming you've required 'rack/coffee' somehow, stick this in the 18 | Rails initializer config block: 19 | 20 | config.middleware.use Rack::Coffee, :root => "#{RAILS_ROOT}/public" 21 | 22 | Note however that by default this will not play nicely with 23 | `javascript_include_tag`'s `:cache` option, you would need to compile your 24 | .coffee files before deploying. Alternately, use the rails asset pipeline. 25 | 26 | ## Options 27 | 28 | * `:root`: the directory above `urls`. Defaults to `Dir.pwd`. 29 | * `:urls`: the directories in which to look for coffeescripts. May specify 30 | a string or an array of strings. Defaults to `/javascripts`. 31 | * `:cache-compile`: When truthy, will create and look for tempfiles with the 32 | timestamp of the desired coffee file, and use those. Meant to speed up 33 | development of projects with lots of coffee files. 34 | * `:cache-control`: Sets a `Cache-Control` header if present. Defaults to false. 35 | Values are interpreted like so: 36 | - `true`: max-age=86400 37 | - `3600`: max-age=3600 38 | - `:public` or `'public'`: max-age=86400, public 39 | - `[3600, :public]` or `%w(3600 public)`: max-age=3600, public 40 | - `false` or `nil`: disables cache header 41 | * `:bare`: When `true`, disables the top-level function wrapper that 42 | CoffeeScript uses by default. 43 | * `:join`: Set to a string, f.e. "index" to concat all the .coffee files before 44 | compiling 45 | 46 | ## Bugs? 47 | 48 | * Let me know here: [Issue Tracking][issues] 49 | 50 | ## Requirements 51 | 52 | * [CoffeeScript Gem][coffee-gem] and therefore [execjs][] 53 | * [Rack][rack] 54 | 55 | ## History 56 | 57 | * March 28, 2014: 58 | Release 1.0.3. Requests now return a Content-Length header, and are 59 | therefore valid without needing something like Rack::ContentLength. Thanks 60 | to [Naksu](https://github.com/naksu) for the fix. 61 | 62 | * August 14, 2013: 63 | Release 1.0.2. Fixes a bug whereby bad things happen when the cache-compile 64 | directory is deleted. Thanks to [Tomas Rojas](https://github.com/tmsrjs) for 65 | the fix. 66 | 67 | * July 13, 2013: 68 | Release 1.01. Add "Licence" field to gemspec. See [this][gemspec-license] 69 | for more info. As someone who recently had to do a license audit, 70 | I appreciate having this available. 71 | 72 | [gemspec-license]: http://www.benjaminfleischer.com/2013/07/12/make-the-world-a-better-place-put-a-license-in-your-gemspec/ 73 | 74 | * March 18, 2012: 75 | This release is **NOT BACKWARDS COMPATIBLE** with 0.9.x. 76 | * added a `:cache_compile` option. If truthy, will cache the compiled coffee 77 | to a tempfile timestamped with the modification time of the original. 78 | * **BACKWARDS INCOMPATIBILITY** chance the `:cache` and `:ttl` options into 79 | `:cache_control`. See documentation above for how the arguments work. 80 | * A fair bit of refactoring to make the main call method easier to follow. 81 | * **BACKWARDS INCOMPATIBILITY** remove `:static` option. If you want to 82 | serve stock javascript files from the same directory as your coffeescript 83 | files, stick a Rack::File in your middleware stack after Rack::Coffee. 84 | * **BACKWARDS INCOMPATIBILITY** remove `:nowrap` option in favor of `:bare` 85 | * Use `execjs` gem instead of coffee-script command. Thanks to [jewel][] for 86 | kicking this off, even if I didn't use their code. 87 | 88 | * May 5, 2011: release 0.9.1 89 | * Fix a bug in the 'join' option to reflect how command-line -p actually 90 | works 91 | 92 | * May 3, 2011: release 0.9 93 | * Make '304 NOT MODIFIED' return a correct response body on Ruby 1.9 94 | [Aanand Prasad][aanand] 95 | * Added 'join' option for concating your js 96 | 97 | * January 21, 2011: release 0.3.3 98 | Two changes by [Jonathan Baudanza][jbaudanza]: 99 | * changed --nowrap to --bare, per a recent change to coffee-script. You may 100 | use :nowrap or :bare to indicate you want this 101 | * return 304 NOT MODIFIED for caching purposes 102 | 103 | * March 21, 2010: release 0.3.2 104 | * added :nowrap option to config, allowing the disabling of the top-level 105 | function wrapper. 106 | 107 | * March 6, 2010: release 0.3.1 108 | * options now take :cache and :ttl options for setting cache headers, should 109 | you decide to actually serve up hot coffeescripts outside of your 110 | development environment. Via Brian Mitchell. 111 | 112 | * March 6, 2010: release 0.3 REQUIRES COFFEE-SCRIPT 0.5 OR HIGHER 113 | * CoffeeScript is now written in coffeescript. The included compiler is now 114 | based on node.js instead of being hosted in a ruby gem, so we're shelling 115 | out to the command-line interpreter. Thanks to [Brian Mitchell][binary42] 116 | for doing most of the dirty work, at least as far as ruby 1.9 is 117 | concerned. 118 | 119 | * January 27, 2010: release 0.2 BACKWARDS INCOMPATIBLE 120 | * replace :url parameter in favor of :urls, now it behaves similarly to 121 | Rack::Static (Brian Mitchell) 122 | * add :static parameter, which when false will disable automatic asset 123 | serving of url misses via Rack::File, instead passing through to the app. 124 | * improve documentation for Rails 125 | * remove dependency on Pathname, oh if only it were stdlib instead of extlib 126 | 127 | * January 26, 2010: First public release 0.1. 128 | 129 | ## Copyright 130 | 131 | Copyright (C) 2010 Matthew Lyon 132 | 133 | Permission is hereby granted, free of charge, to any person obtaining a copy 134 | of this software and associated documentation files (the "Software"), to 135 | deal in the Software without restriction, including without limitation the 136 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 137 | sell copies of the Software, and to permit persons to whom the Software is 138 | furnished to do so, subject to the following conditions: 139 | 140 | The above copyright notice and this permission notice shall be included in 141 | all copies or substantial portions of the Software. 142 | 143 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 144 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 145 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 146 | THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 147 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 148 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 149 | 150 | [coffee]: http://jashkenas.github.com/coffee-script/ 151 | [coffee-gem]: https://github.com/josh/ruby-coffee-script 152 | [execjs]: https://github.com/sstephenson/execjs 153 | [issues]: http://github.com/mattly/rack-coffee/issues 154 | [rack]: http://rack.rubyforge.org/ 155 | [binary42]: http://github.com/binary42 156 | [jbaudanza]: https://github.com/jbaudanza 157 | [aanand]: https://github.com/aanand 158 | [jewel]: https://github.com/jewel 159 | -------------------------------------------------------------------------------- /lib/rack/coffee.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | 3 | # look folks, Pathname is in extlib. Learn it, use it. 4 | require 'pathname' 5 | 6 | require 'rack/utils' 7 | require 'coffee_script' 8 | 9 | module Rack 10 | class Coffee 11 | 12 | CACHE_CONTROL_TTL_DEFAULT = 86400 13 | 14 | attr_accessor :app, :urls, :root, :cache_compile_dir, 15 | :compile_without_closure, :concat_to_file, :cache_control 16 | 17 | def initialize(app, opts={}) 18 | @app = app 19 | @urls = [opts.fetch(:urls, '/javascripts')].flatten 20 | @root = Pathname.new(opts.fetch(:root) { Dir.pwd }) 21 | set_cache_header_opts(opts.fetch(:cache_control, false)) 22 | @concat_to_file = opts.fetch(:join, false) 23 | @concat_to_file += '.coffee' if @concat_to_file 24 | @cache_compile_dir = if opts.fetch(:cache_compile, false) 25 | Pathname.new(Dir.mktmpdir) 26 | else 27 | nil 28 | end 29 | @compile_without_closure = opts.fetch(:bare, false) 30 | end 31 | 32 | def set_cache_header_opts(given) 33 | given = [given].flatten.map{|i| String(i) } 34 | return if ['false', ''].include?(given.first) 35 | ttl = given.first.to_i > 0 ? given.shift : CACHE_CONTROL_TTL_DEFAULT 36 | pub = given.first == 'public' ? ', public' : '' 37 | @cache_control = "max-age=#{ttl}#{pub}" 38 | end 39 | 40 | def brew(file) 41 | if cache_compile_dir 42 | cache_compile_dir.mkpath 43 | cache_file = cache_compile_dir + "#{file.mtime.to_i}_#{file.basename}" 44 | if cache_file.file? 45 | cache_file.read 46 | else 47 | brewed = compile(file.read) 48 | cache_file.open('w') {|f| f << brewed } 49 | brewed 50 | end 51 | else 52 | compile(file.read) 53 | end 54 | end 55 | 56 | def compile(coffee) 57 | CoffeeScript.compile coffee, {:bare => compile_without_closure } 58 | end 59 | 60 | def not_modified 61 | [304, {}, ["Not modified"]] 62 | end 63 | 64 | def forbidden 65 | [403, {'Content-Type' => 'text/plain'}, ["Forbidden\n"]] 66 | end 67 | 68 | def check_modified_since(rack_env, last_modified) 69 | cache_time = rack_env['HTTP_IF_MODIFIED_SINCE'] 70 | cache_time && last_modified <= Time.parse(cache_time) 71 | end 72 | 73 | def headers_for(mtime, contents) 74 | headers = { 75 | 'Content-Type' => 'application/javascript', 76 | 'Last-Modified' => mtime.httpdate, 77 | 'Content-Length' => contents.bytesize.to_s 78 | } 79 | headers['Cache-Control'] = @cache_control if @cache_control 80 | headers 81 | end 82 | 83 | def own_path?(path) 84 | path =~ /\.js$/ && urls.any? {|url| path.index(url) == 0} 85 | end 86 | 87 | def call(env) 88 | path = Utils.unescape(env["PATH_INFO"]) 89 | return app.call(env) unless own_path?(path) 90 | return forbidden if path.include?('..') 91 | desired_file = root + path.sub(/\.js$/, '.coffee').sub(%r{^/},'') 92 | if concat_to_file == String(desired_file.basename) 93 | source_files = Pathname.glob("#{desired_file.dirname}/*.coffee") 94 | elsif desired_file.file? 95 | source_files = [desired_file] 96 | else 97 | return app.call(env) 98 | end 99 | last_modified = source_files.map {|file| file.mtime }.max 100 | return not_modified if check_modified_since(env, last_modified) 101 | brewed = source_files.map{|file| brew(file) }.join("\n") 102 | [200, headers_for(last_modified, brewed), [brewed]] 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /rack-coffee.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "rack-coffee" 5 | s.version = "1.0.3" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Matthew Lyon", "Brian Mitchell"] 9 | s.date = "2014-03-29" 10 | s.description = "Rack Middlware for compiling and serving .coffee files using coffee-script; \"/javascripts/app.js\" compiles and serves \"/javascipts/app.coffee\"." 11 | s.email = "matthew@lyonheart.us" 12 | s.files = [".gitignore", ".travis.yml", "Gemfile", "Gemfile.lock", "Rakefile", "Readme.mkdn", "lib/rack/coffee.rb", "rack-coffee.gemspec", "test/javascripts/cache_compile.coffee", "test/javascripts/static.js", "test/javascripts/test.coffee", "test/other_javascripts/test.coffee", "test/rack_coffee_test.rb"] 13 | s.homepage = "http://github.com/mattly/rack-coffee" 14 | s.licenses = ["MIT"] 15 | s.require_paths = ["lib"] 16 | s.rubyforge_project = "rack-coffee" 17 | s.rubygems_version = "2.0.3" 18 | s.summary = "serve up coffeescript from rack middleware" 19 | s.test_files = ["test/rack_coffee_test.rb"] 20 | 21 | if s.respond_to? :specification_version then 22 | s.specification_version = 4 23 | 24 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 25 | s.add_runtime_dependency(%q, [">= 0"]) 26 | s.add_runtime_dependency(%q, [">= 0"]) 27 | else 28 | s.add_dependency(%q, [">= 0"]) 29 | s.add_dependency(%q, [">= 0"]) 30 | end 31 | else 32 | s.add_dependency(%q, [">= 0"]) 33 | s.add_dependency(%q, [">= 0"]) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/javascripts/cache_compile.coffee: -------------------------------------------------------------------------------- 1 | alert("version one") -------------------------------------------------------------------------------- /test/javascripts/static.js: -------------------------------------------------------------------------------- 1 | alert("static"); -------------------------------------------------------------------------------- /test/javascripts/test.coffee: -------------------------------------------------------------------------------- 1 | alert("coffee¿") -------------------------------------------------------------------------------- /test/other_javascripts/test.coffee: -------------------------------------------------------------------------------- 1 | alert("other coffee") -------------------------------------------------------------------------------- /test/rack_coffee_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'test/unit' 4 | begin 5 | require 'rack/mock' 6 | require 'rack/lint' 7 | rescue LoadError 8 | require 'rubygems' 9 | retry 10 | end 11 | 12 | require File.dirname(__FILE__) + "/../lib/rack/coffee" 13 | 14 | class DummyApp 15 | def call(env) 16 | [201, {"Content-Type" => "text/plain"}, ["Default Response"]] 17 | end 18 | end 19 | 20 | class RackCoffeeTest < Test::Unit::TestCase 21 | 22 | attr_reader :compiled_body_regex 23 | 24 | def setup 25 | @root = File.expand_path(File.dirname(__FILE__)) 26 | @options = {:root => @root} 27 | @compiled_body_regex = /function.*alert\(\"coffee¿\"\)\;.*this/m 28 | end 29 | 30 | def request(options={}) 31 | options = @options.merge(options) 32 | Rack::MockRequest.new(Rack::Lint.new(Rack::Coffee.new(DummyApp.new, options))) 33 | end 34 | 35 | def test_serves_coffeescripts 36 | result = request.get("/javascripts/test.js") 37 | assert_equal 200, result.status 38 | assert_match compiled_body_regex, result.body 39 | assert_equal File.mtime("#{@root}/javascripts/test.coffee").httpdate, result["Last-Modified"] 40 | assert_equal result.body.bytesize.to_s, result["Content-Length"] 41 | end 42 | 43 | def test_calls_app_on_coffee_miss 44 | result = request.get("/javascripts/static.js") 45 | assert_equal 201, result.status 46 | assert_equal "Default Response", result.body 47 | end 48 | 49 | def test_calls_app_on_path_miss 50 | result = request.get("/hello") 51 | assert_equal 201, result.status 52 | assert_equal "Default Response", result.body 53 | end 54 | 55 | def test_not_modified_response 56 | modified_time = (File.mtime("#{@root}/javascripts/test.coffee") + 1).httpdate 57 | result = request.get("/javascripts/test.js", 'HTTP_IF_MODIFIED_SINCE' => modified_time ) 58 | assert_equal 304, result.status 59 | assert_equal 'Not modified', result.body 60 | end 61 | 62 | def test_does_not_allow_directory_traversal 63 | result = request.get("/javascripts/../README.js") 64 | assert_equal 403, result.status 65 | end 66 | 67 | def test_does_not_allow_directory_travesal_with_encoded_periods 68 | result = request.get("/javascripts/%2E%2E/README.js") 69 | assert_equal 403, result.status 70 | end 71 | 72 | def test_serves_coffeescripts_with_alternate_options 73 | result = request({:root => File.expand_path(File.dirname(__FILE__)), :urls => "/other_javascripts"}).get("/other_javascripts/test.js") 74 | assert_equal 200, result.status 75 | assert_match /alert\(\"other coffee\"\)\;/, result.body 76 | end 77 | 78 | def test_cache_control_defaults 79 | result = request({:cache_control => true}).get("/javascripts/test.js") 80 | cache = result.headers["Cache-Control"] 81 | assert_not_nil cache 82 | assert_equal "max-age=86400", cache 83 | end 84 | 85 | def test_cache_control_with_options 86 | result = request({:cache_control => %w(300 public)}).get("/javascripts/test.js") 87 | cache = result.headers["Cache-Control"] 88 | assert_not_nil cache 89 | assert_match /max-age=300/, cache 90 | assert_match /, public/, cache 91 | end 92 | 93 | def test_cache_control_option_parsing 94 | [ [300, "max-age=300"], ['300', "max-age=300"], 95 | [:public, "max-age=86400, public"], ['public', "max-age=86400, public"], 96 | [[300, :public], "max-age=300, public"], 97 | [%w(300 public), "max-age=300, public"] 98 | ].each do |given, expected| 99 | middleware = Rack::Coffee.new(DummyApp, {:cache_control => given}) 100 | assert_equal expected, middleware.cache_control 101 | end 102 | end 103 | 104 | def test_bare_option 105 | result = request({:bare => true}).get("/javascripts/test.js") 106 | assert_equal "alert(\"coffee¿\");", result.body.strip 107 | end 108 | 109 | def test_join_option_with_join 110 | result = request({:join => 'index'}).get("/javascripts/index.js") 111 | assert_equal 200, result.status 112 | end 113 | 114 | def test_join_option_with_file 115 | result = request({:join => 'index'}).get("/javascripts/test.js") 116 | assert_equal 200, result.status 117 | assert_match compiled_body_regex, result.body.strip 118 | end 119 | 120 | def test_cache_compile_option 121 | Dir.mktmpdir do |root| 122 | FileUtils.mkdir "#{root}/javascripts" 123 | app = Rack::Coffee.new(DummyApp, {:root => root, :cache_compile => true}) 124 | get = lambda{|path| Rack::MockRequest.new(Rack::Lint.new(app)).get(path) } 125 | dir = app.cache_compile_dir 126 | path = File.join(root, 'javascripts/cache_compile.coffee') 127 | File.open(path,'w') {|f| f.write 'alert("version one")' } 128 | oldtime = Time.local(2010, 11, 23, 19, 0) 129 | File.utime(oldtime, oldtime, path) 130 | # 1: is it creating the cached compiled file? 131 | result = get.call('/javascripts/cache_compile.js') 132 | cache_file = File.join(dir, "#{oldtime.to_i}_cache_compile.coffee") 133 | assert File.exist?(cache_file) 134 | assert_equal 200, result.status 135 | # 2: will it presumably re-use it if it hasn't changed? 136 | # yes, I'm using sleep in my tests. Deal with it. 137 | cache_mtime = File.mtime(cache_file) 138 | sleep 1 139 | get.call('/javascripts/cache_compile.js') 140 | assert_equal File.mtime(cache_file), cache_mtime 141 | # 3: will it overwrite it if the underlying contents change? 142 | File.open(path, 'w') {|f| f.write 'alert("version two")' } 143 | result = get.call('/javascripts/cache_compile.js') 144 | assert_equal 200, result.status 145 | mtime = Time.parse(result.headers['Last-Modified']) 146 | assert_not_equal mtime, oldtime 147 | assert File.exist?(File.join(dir, "#{mtime.to_i}_cache_compile.coffee")) 148 | end 149 | end 150 | 151 | def test_does_not_break_if_cache_compile_dir_is_deleted 152 | Dir.mktmpdir do |root| 153 | FileUtils.mkdir "#{root}/javascripts" 154 | app = Rack::Coffee.new(DummyApp, {:root => root, :cache_compile => true}) 155 | get = lambda{|path| Rack::MockRequest.new(Rack::Lint.new(app)).get(path) } 156 | path = File.join(root, 'javascripts/cache_compile.coffee') 157 | File.open(path,'w') {|f| f.write 'alert("version one")' } 158 | dir = app.cache_compile_dir 159 | get.call('/javascripts/cache_compile.js') 160 | dir.rmtree 161 | assert_nothing_raised Errno::ENOENT do 162 | get.call('/javascripts/cache_compile.js') 163 | end 164 | assert dir.exist? 165 | end 166 | end 167 | end 168 | --------------------------------------------------------------------------------