├── .gitignore ├── .groc.json ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── lib ├── rails-dev-tweaks.rb └── rails_dev_tweaks │ ├── configuration.rb │ ├── granular_autoload │ ├── matchers │ │ ├── all_matcher.rb │ │ ├── asset_matcher.rb │ │ ├── forced_matcher.rb │ │ ├── path_matcher.rb │ │ └── xhr_matcher.rb │ └── middleware.rb │ ├── railtie.rb │ └── version.rb └── rails-dev-tweaks.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | 6 | -------------------------------------------------------------------------------- /.groc.json: -------------------------------------------------------------------------------- 1 | { 2 | "globs": ["lib/**/*.rb", "README.md"], 3 | "github": true 4 | } 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec 4 | 5 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Wavii, inc. http://wavii.com/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 6 | persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 9 | Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 12 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 13 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 14 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rails-dev-tweaks 2 | ================ 3 | 4 | A collection of tweaks to improve your Rails (3.1+) development experience. 5 | 6 | To install, simply add it to your gemfile: 7 | 8 | gem 'rails-dev-tweaks', '~> 1.1' 9 | 10 | And review the following section to make sure that `rails-dev-tweaks` is 11 | configured the way you expect: 12 | 13 | 14 | Intended Usage (and Caveats) 15 | ---------------------------- 16 | 17 | This gem is intended to provide a default configuration that covers most rails 18 | apps: 19 | 20 | * _All_ asset requests _will not_ reload your app's code. This is probably only 21 | a problem if you are using custom sass functions, or otherwise referencing 22 | your app from within assets. 23 | 24 | * XHR requests **will reload** your app's code. (This was not the case in prior 25 | versions of `rails-dev-tweaks`) 26 | 27 | If any of these points don't work out for you, don't fret! You can override the 28 | defaults with some simple configuration tweaks to your environment. Read on: 29 | 30 | 31 | Granular Autoload 32 | ================= 33 | 34 | You can specify autoload rules for your app via a configuration block in your 35 | application or environment configuration. These rules are specified via 36 | exclusion (`skip`) and inclusion (`keep`). Rules defined later override those 37 | defined before. 38 | 39 | config.dev_tweaks.autoload_rules do 40 | # You can used named matchers (see below). This particular matcher 41 | # effectively clears any default matchers 42 | keep :all 43 | 44 | # Exclude all requests that begin with /search 45 | skip '/search' 46 | # But include routes that include smerch 47 | keep /smerch/ 48 | 49 | # Use a block if you want to inspect the request 50 | skip {|request| request.post?} 51 | end 52 | 53 | The default autoload rules should cover most development patterns: 54 | 55 | config.dev_tweaks.autoload_rules do 56 | keep :all 57 | 58 | skip '/favicon.ico' 59 | skip :assets 60 | keep :forced 61 | end 62 | 63 | By default, every request that skips the autoload hooks will generate an 64 | additional log line saying so in an effort to be transparent about what is going 65 | on. If you prefer, you can disable that log message to keep things a bit more 66 | tidy in your logs: 67 | 68 | config.dev_tweaks.log_autoload_notice = false 69 | 70 | 71 | Named Matchers 72 | -------------- 73 | 74 | Named matchers are classes defined under 75 | RailsDevTweaks::GranularAutoload::Matchers:: and simply define a call method 76 | that is given a ActionDispatch::Request and returns true/false on whether that 77 | request matches. Match names are converted into a module name via 78 | "#{name.to\_s.classify}Matcher". E.g. :assets will specify 79 | `RailsDevTweaks::GranularAutoload::Matchers::AssetMatcher`. 80 | 81 | Any additional arguments given to a `skip` or `keep` call will be passed as 82 | initializer arguments to the matcher. 83 | 84 | 85 | ### :all 86 | 87 | Matches every request passed to it. 88 | 89 | 90 | ### :assets 91 | 92 | Rails 3.1 integrated [Sprockets](http://getsprockets.org/) as its asset 93 | packager. Unfortunately, since the asset packager is mounted using the 94 | traditional Rails dispatching infrastructure, it's hidden behind the Rails 95 | autoloader (unloader). This matcher will match any requests that are routed to 96 | Sprockets (specifically any mounted Sprockets::Base instance). 97 | 98 | 99 | ### :forced 100 | 101 | To aid in live-debugging when you need to, this matcher will match any request 102 | that has `force_autoload` set as a parameter (GET or POST), or that has the 103 | `Force-Autoload` header set to something. 104 | 105 | If you are live-debugging jQuery ajax requests, this helpful snippet will turn 106 | on forced autoloading for the remainder of the browser's session: 107 | 108 | $.ajaxSetup({"beforeSend": function(xhr) {xhr.setRequestHeader("Force-Autoload", "true")} }) 109 | 110 | 111 | ### :path 112 | 113 | Matches the path of the request via a regular expression. 114 | 115 | keep :path, /thing/ # Match any request with "thing" in the path. 116 | 117 | Note that `keep '/stuff'` is just shorthand for `keep :path, /^\/stuff/`. 118 | Similarly, `keep /thing/` is shorthand for `keep :path, /thing/` 119 | 120 | 121 | ### :xhr 122 | 123 | Matches any XHR request (via request.xhr?). The assumption here is that you 124 | generally don't live-debug your XHR requests, and are instead refreshing the 125 | page that kicks them off before running against new response code. 126 | 127 | 128 | License 129 | ======= 130 | 131 | `rails-dev-tweaks` is MIT licensed by Wavii, Inc. http://wavii.com 132 | 133 | See the accompanying file, `MIT-LICENSE`, for the full text. 134 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | -------------------------------------------------------------------------------- /lib/rails-dev-tweaks.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | require 'active_support/dependencies' 3 | 4 | module RailsDevTweaks 5 | LIB_PATH = File.expand_path('..', __FILE__) 6 | end 7 | 8 | # Ironically, we use autoloading ourselves to enforce our file structure, and less typing. 9 | ActiveSupport::Dependencies.autoload_paths << RailsDevTweaks::LIB_PATH 10 | ActiveSupport::Dependencies.autoload_once_paths << RailsDevTweaks::LIB_PATH # But don't allow *auto-reloading*! 11 | 12 | # Reference the railtie to force it to load 13 | RailsDevTweaks::Railtie 14 | -------------------------------------------------------------------------------- /lib/rails_dev_tweaks/configuration.rb: -------------------------------------------------------------------------------- 1 | class RailsDevTweaks::Configuration 2 | 3 | # By default, we log a notice on each request that has its to_prepare hooks skipped, you can 4 | # disable that if you choose! 5 | attr_accessor :log_autoload_notice 6 | 7 | attr_reader :granular_autoload_config 8 | 9 | def initialize 10 | @log_autoload_notice = true 11 | 12 | @granular_autoload_config = GranularAutoloadConfiguration.new 13 | 14 | # And set our defaults 15 | self.autoload_rules do 16 | keep :all 17 | 18 | skip '/favicon.ico' 19 | skip :assets 20 | keep :forced 21 | end 22 | end 23 | 24 | # Takes a block that configures the granular autoloader's rules. 25 | def autoload_rules(&block) 26 | @granular_autoload_config.instance_eval(&block) 27 | end 28 | 29 | class GranularAutoloadConfiguration 30 | 31 | def initialize 32 | # Each rule is a simple pair: [:skip, callable], [:keep, callable], etc. 33 | @rules = [] 34 | end 35 | 36 | def keep(*args, &block) 37 | self.append_rule(:keep, *args, &block) 38 | end 39 | 40 | def skip(*args, &block) 41 | self.append_rule(:skip, *args, &block) 42 | end 43 | 44 | def append_rule(rule_type, *args, &block) 45 | unless rule_type == :skip || rule_type == :keep 46 | raise TypeError, "Rule must be :skip or :keep. Got: #{rule_type.inspect}" 47 | end 48 | 49 | # Simple matcher blocks 50 | if args.size == 0 && block.present? 51 | @rules.unshift [rule_type, block] 52 | return self 53 | end 54 | 55 | # String match shorthand 56 | args[0] = /^#{args[0]}/ if args.size == 1 && args[0].kind_of?(String) 57 | 58 | # Regex match shorthand 59 | args = [:path, args[0]] if args.size == 1 && args[0].kind_of?(Regexp) 60 | 61 | if args.size == 0 && block.blank? 62 | raise TypeError, 'Cannot process autoload rule as specified. Expecting a named matcher (symbol), path prefix (string) or block' 63 | end 64 | 65 | # Named matcher 66 | matcher_class = "RailsDevTweaks::GranularAutoload::Matchers::#{args[0].to_s.classify}Matcher".constantize 67 | matcher = matcher_class.new(*args[1..-1]) 68 | raise TypeError, "Matchers must respond to :call. #{matcher.inspect} does not." unless matcher.respond_to? :call 69 | 70 | @rules.unshift [rule_type, matcher] 71 | 72 | self 73 | end 74 | 75 | def should_reload?(request) 76 | @rules.each do |rule_type, callable| 77 | return rule_type == :keep if callable.call(request) 78 | end 79 | 80 | # We default to reloading to preserve behavior, but we should never get to this unless the configuration is 81 | # all sorts of horked. 82 | true 83 | end 84 | 85 | end 86 | 87 | end 88 | -------------------------------------------------------------------------------- /lib/rails_dev_tweaks/granular_autoload/matchers/all_matcher.rb: -------------------------------------------------------------------------------- 1 | class RailsDevTweaks::GranularAutoload::Matchers::AllMatcher 2 | 3 | def call(request) 4 | true 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /lib/rails_dev_tweaks/granular_autoload/matchers/asset_matcher.rb: -------------------------------------------------------------------------------- 1 | class RailsDevTweaks::GranularAutoload::Matchers::AssetMatcher 2 | 3 | def call(request) 4 | route_engine = request.headers['action_dispatch.routes'] 5 | 6 | if route_engine.respond_to?(:router) 7 | mounted_app = journey_find_app(route_engine.router, request) 8 | else 9 | mounted_app = rack_find_app(route_engine.set.dup, request) 10 | end 11 | 12 | # What do we have? 13 | mounted_app.is_a? Sprockets::Base 14 | end 15 | 16 | def journey_find_app(router, request) 17 | router.recognize(request) do |route, *args| 18 | return route.app 19 | end 20 | end 21 | 22 | def rack_find_app(router, request) 23 | main_mount = router.recognize(request) 24 | 25 | # Unwind until we have an actual app 26 | while main_mount != nil 27 | if main_mount.is_a? Array 28 | main_mount = main_mount.first 29 | 30 | elsif main_mount.is_a? Rack::Mount::Route 31 | main_mount = main_mount.app 32 | 33 | elsif main_mount.is_a? Rack::Mount::Prefix 34 | # Bah, no accessor here 35 | main_mount = main_mount.instance_variable_get(:@app) 36 | 37 | # Well, we got something 38 | else 39 | break 40 | end 41 | end 42 | 43 | main_mount 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /lib/rails_dev_tweaks/granular_autoload/matchers/forced_matcher.rb: -------------------------------------------------------------------------------- 1 | class RailsDevTweaks::GranularAutoload::Matchers::ForcedMatcher 2 | 3 | def call(request) 4 | request.headers['Force-Autoload'] || request.params.has_key?(:force_autoload) 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /lib/rails_dev_tweaks/granular_autoload/matchers/path_matcher.rb: -------------------------------------------------------------------------------- 1 | class RailsDevTweaks::GranularAutoload::Matchers::PathMatcher 2 | 3 | def initialize(regex) 4 | @regex = regex 5 | end 6 | 7 | def call(request) 8 | @regex =~ request.fullpath 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /lib/rails_dev_tweaks/granular_autoload/matchers/xhr_matcher.rb: -------------------------------------------------------------------------------- 1 | class RailsDevTweaks::GranularAutoload::Matchers::XhrMatcher 2 | 3 | def call(request) 4 | request.xhr? 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /lib/rails_dev_tweaks/granular_autoload/middleware.rb: -------------------------------------------------------------------------------- 1 | # Here's an idea, let's not reload the entire dev environment for each asset request. Let's only do that on regular 2 | # content requests. 3 | class RailsDevTweaks::GranularAutoload::Middleware 4 | 5 | # Don't cleanup before the very first request 6 | class << self 7 | attr_writer :processed_a_request 8 | def processed_a_request? 9 | @processed_a_request 10 | end 11 | end 12 | 13 | def initialize(app) 14 | @app = app 15 | end 16 | 17 | def call(env) 18 | request = ActionDispatch::Request.new(env.dup) 19 | 20 | # reload, or no? 21 | if Rails.application.config.dev_tweaks.granular_autoload_config.should_reload?(request) 22 | # Confusingly, we flip the request prepare/cleanup life cycle around so that we're only cleaning up on those 23 | # requests that want to be reloaded 24 | 25 | # No-op if this is the first request. The initializers take care of that one. 26 | if self.class.processed_a_request? && reload_dependencies? 27 | ActionDispatch::Reloader.cleanup! 28 | ActionDispatch::Reloader.prepare! 29 | end 30 | self.class.processed_a_request = true 31 | 32 | elsif Rails.application.config.dev_tweaks.log_autoload_notice 33 | Rails.logger.info 'RailsDevTweaks: Skipping ActionDispatch::Reloader hooks for this request.' 34 | end 35 | 36 | return @app.call(env) 37 | end 38 | 39 | private 40 | 41 | def reload_dependencies? 42 | application = Rails.application 43 | 44 | # Rails 3.2 defines reload_dependencies? and it only reloads if reload_dependencies? returns true. 45 | (!application.class.method_defined?(:reload_dependencies?) || 46 | application.send(:reload_dependencies?)) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/rails_dev_tweaks/railtie.rb: -------------------------------------------------------------------------------- 1 | class RailsDevTweaks::Railtie < Rails::Railtie 2 | 3 | config.dev_tweaks = RailsDevTweaks::Configuration.new 4 | 5 | config.before_initialize do |app| 6 | # We can't inspect the stack because it's deferred... For now, just assume we have it when config.cache_clasess is 7 | # falsy; which should always be the case in the current version of rails anyway. 8 | unless app.config.cache_classes 9 | app.config.middleware.swap ActionDispatch::Reloader, RailsDevTweaks::GranularAutoload::Middleware 10 | end 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /lib/rails_dev_tweaks/version.rb: -------------------------------------------------------------------------------- 1 | module RailsDevTweaks 2 | VERSION = '1.2.0' 3 | end 4 | -------------------------------------------------------------------------------- /rails-dev-tweaks.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path('../lib', __FILE__) 3 | require 'rails_dev_tweaks/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'rails-dev-tweaks' 7 | s.version = RailsDevTweaks::VERSION 8 | s.authors = ['Wavii, Inc.'] 9 | s.email = ['info@wavii.com'] 10 | s.homepage = 'http://wavii.com/' 11 | s.summary = %q{A collection of tweaks to improve your Rails (3.1+) development experience.} 12 | s.description = %q{A collection of tweaks to improve your Rails (3.1+) development experience.} 13 | s.license = 'MIT' 14 | 15 | s.rubyforge_project = 'rails-dev-tweaks' 16 | 17 | s.files = Dir['lib/**/*'] + ['MIT-LICENSE', 'Rakefile', 'README.md'] 18 | s.require_paths = ['lib'] 19 | 20 | s.add_runtime_dependency 'railties', '>= 3.1' 21 | s.add_runtime_dependency 'actionpack', '>= 3.1' 22 | end 23 | --------------------------------------------------------------------------------