├── .gitignore ├── .rspec ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── breakfast.gemspec ├── lib ├── breakfast.rb ├── breakfast │ ├── brunch_watcher.rb │ ├── compilation_listener.rb │ ├── helper.rb │ ├── live_reload_channel.rb │ ├── local_environment.rb │ ├── manifest.rb │ ├── railtie.rb │ ├── status_channel.rb │ └── version.rb ├── generators │ └── breakfast │ │ ├── install_generator.rb │ │ └── templates │ │ ├── app.js │ │ ├── app.scss │ │ ├── brunch-config.js │ │ └── package.json └── tasks │ └── breakfast.rake ├── node_package ├── .eslintrc.json ├── .npmignore ├── package.json └── src │ ├── breakfast-rails.js │ ├── live-reload.js │ ├── settings.js │ └── status-bar.js └── spec ├── acceptance └── install_spec.rb ├── breakfast ├── compilation_listener_spec.rb └── manifest_spec.rb ├── breakfast_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | node_package/node_modules/* 11 | node_package/lib/* 12 | npm-debug.log 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: ruby 3 | rvm: 4 | - 2.3.1 5 | before_install: 6 | - gem install bundler -v 1.12.5 7 | - nvm install 7.4.0 8 | - nvm use 7.4.0 9 | # Repo for Yarn 10 | - sudo apt-key adv --keyserver pgp.mit.edu --recv D101F7899D41F3C3 11 | - echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list 12 | - sudo apt-get update -qq 13 | - sudo apt-get install -y -qq yarn 14 | - yarn install 15 | 16 | cache: 17 | directories: 18 | - $HOME/.yarn-cache 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGE LOG 2 | 3 | ### 0.6.6 - 2019-04-26 4 | 5 | #### Changes 6 | 7 | - Support Rails 6 by removing constraint on ActionCable 8 | 9 | @mattr 10 | 11 | ### 0.6.5 - 2019-04-04 12 | 13 | #### Changes 14 | 15 | - Make Rake Task System Calls Fail Loudly 16 | 17 | @mcclayton 18 | 19 | ### 0.6.3 - 2018-08-14 20 | 21 | #### Fixed 22 | 23 | - Typo in install rake task 24 | 25 | @karmiclychee 26 | 27 | ### 0.6.3 - 2018-08-14 28 | 29 | #### Fixed 30 | 31 | - Potential double yarn install 32 | - assets:precompile not defined properly (when no Sprockets) 33 | 34 | ### 0.6.2 - 2017-05-11 35 | 36 | #### Fixed 37 | 38 | - Typo in install generator. Add brunch-babel package 39 | 40 | @haffla 41 | 42 | ### 0.6.1 - 2017-05-04 43 | 44 | #### Fixed 45 | 46 | - Potential Heroku bug where assets would be cleared out on deploy. 47 | 48 | ### 0.6.0 - 2017-03-12 49 | 50 | #### Upgrading to `0.6.0` from `0.5.x` 51 | 52 | - Update gem with `bundle update breakfast` 53 | - Update the JS package with `yarn upgrade breakfast-rails` 54 | - If deploying with Capistrano, remove `require "breakfast/capistrano"` from 55 | your `Capfile`. Remove any custom Breakfast settings from `config/deploy.rb`. 56 | Ensure that you are using [Capistrano Rails](https://github.com/capistrano/rails) 57 | and have `require 'capistrano/rails'` or `require 'capistrano/rails/assets'` 58 | in your `Capfile`. 59 | - If deploying with Heroku, run the following commands: 60 | 1. heroku buildpacks:clear 61 | 2. heroku buildpacks:set heroku/nodejs --index 1 62 | 3. heroku buildpacks:set heroku/ruby --index 2 63 | 64 | #### Fixed 65 | 66 | - Puma hanging in clustered mode. Breakfast would fail to cleanly exit on Puma 67 | exit, causing the server to hang indefinitely. 68 | - Bumped Rails version dependency, can be used with Rails 5.0 and greater. 69 | (Allows usage with Rails 5.1) 70 | 71 | #### Removed 72 | 73 | - Capistrano rake tasks. Previous behavior has been included into the Rails 74 | asset:precompile task. Using the standard [Capistrano Rails](https://github.com/capistrano/rails) 75 | gem is all that required now. 76 | - Need for a custom Heroku buildpack. 77 | 78 | ### 0.5.1 - 2017-02-06 79 | 80 | #### Changed 81 | 82 | If `public/assets` does not exist Breakfast will now create the folder before 83 | attempting to write to it. 84 | 85 | ### 0.5.0 - 2017-02-03 86 | 87 | #### Added 88 | 89 | - Adds support for [Yarn](https://yarnpkg.com/). 90 | - New installs now require Yarn 91 | - Capistrano options `:breakfast_yarn_path` && `:breakfast_yarn_install_command` 92 | 93 | #### Removed 94 | 95 | - NPM client requirement 96 | - Capistrano options `:breakfast_npm_path` && `:breakfast_npm_install_command` 97 | have been removed. 98 | 99 | ### Upgrading 100 | 101 | #### Upgrading to `0.5.0` from `0.4.0` 102 | 103 | - Update gem with `bundle update breakfast` 104 | - Bump the `breakfast-rails` version in `package.json` to `0.5.0` 105 | - Ensure [Yarn](https://yarnpkg.com/docs/install) is installed 106 | - Run `yarn install` 107 | 108 | _Note_ If you are deploying with Capistrano then Yarn is expected to be 109 | installed on 110 | 111 | ### 0.4.0 - 2016-11-14 112 | 113 | #### Upgrading to `0.4.0` from `0.3.0` 114 | 115 | - Update gem with `bundle update breakfast` 116 | - Bump the `breakfast-rails` version in `package.json` to `0.4.0` 117 | - Run `npm install` 118 | 119 | _Note_ Now by default asset fingerprinting will be on by default in production. 120 | A copy of each with the original filename will be present as well, so any 121 | hard-coded links to assets will still work correctly. 122 | 123 | #### Added 124 | 125 | - Asset Digests. Now when deploying assets will have fingerprints added to their 126 | file names. This allows browsers to aggressively cache your assets. 127 | - New Option: `breakfast.manifest.digest`. Defaults to false in development / 128 | test and true everywhere else. When true, enables Rails to serve fingerprinted 129 | assets. 130 | - Rake Commands to trigger certain behavior: 131 | - `breakfast:assets:build` 132 | Manually run a compilation step. 133 | - `breakfast:assets:build_production` 134 | Manually trigger a production build. This will cause assets to get minified. 135 | - `breakfast:assets:digest` 136 | Run through your compiled assets and add a fingerprint to each one. Creates 137 | a copy, leaving a file with the original filename and a duplicate with an 138 | md5 fingerprint. 139 | - `breakfast:assets:clean` 140 | Removes any assets from the output folder that are not specified in the 141 | manifest file (removes out of date files). 142 | - `breakfast:assets:nuke` 143 | Removes manifest and fingerprinted assets from the output folder. 144 | - New Capistrano Option: `:breakfast_npm_install_command` 145 | Defaults to just `install`. Can be overridden to redirect output to dev/null. 146 | Example: 147 | 148 | ``` 149 | set :breakfast_npm_install_command, "install > /dev/null 2>&1" 150 | ``` 151 | 152 | #### Changes 153 | 154 | - Fixed small CSS issue if box-sizing is not set border-box globally. 155 | 156 | #### Contributors 157 | 158 | Many many thanks to the contributors for this release! 159 | 160 | - [@patkoperwas](https://github.com/patkoperwas) 161 | - [@mikeastock](https://github.com/mikeastock) 162 | - [@HParker](https://github.com/HParker) 163 | 164 | ## 0.3.1 - 2016-10-19 165 | 166 | - Better support for determining if Server is running. Using puma, passneger, 167 | etc. instead of the default rails server command now work. 168 | 169 | ## 0.3.0 - 2016-09-28 170 | 171 | ### Upgrading from `0.2.0` 172 | 173 | - Update gem with `bundle update breakfast` 174 | - Bump the `breakfast-rails` version in `package.json` to `0.3.1` 175 | - Run `npm install` 176 | - If you have modified the `config.breakfast.view_folders` option you will need 177 | to replace it. The new option is `config.breakfast.source_code_folders` and it 178 | defaults to `[Rails.root.join("app")]`. If you have view or Ruby files that 179 | you would like to trigger reloads outside of the `app` folder then append 180 | those paths by adding: 181 | 182 | ``` 183 | config.breakfast.source_code_folders << Rails.root.join("lib") 184 | ``` 185 | 186 | To which ever environment you want `Breakfast` to run in 187 | (probably `config/environments/development.rb`). 188 | 189 | ### Added 190 | 191 | - New status bar that allows the user to switch reload strategies on the fly 192 | - Support for Haml & Slim files (without .html extension) 193 | - Reloading on ruby file changes. 194 | - Specify minimum Node & NPM versions when installing (avoid awkward and none 195 | descriptive error messages) 196 | - NPM binary path for Capistrano 197 | 198 | ### Changes 199 | 200 | - config.breakfast.view_folders change to config.breakfast.source_code_folders. 201 | Change brought about by need to trigger reloads when Ruby source code changes. 202 | 203 | ### Removed 204 | 205 | - config.breakfast.view_folders is no longer supported. Deprecated in favor of 206 | source_code_folders option. 207 | 208 | #### Contributors 209 | 210 | Many many thanks to the contributors for this release! 211 | 212 | - [@patkoperwas](https://github.com/patkoperwas) 213 | - [@josh-rosen](https://github.com/Josh-Rosen) 214 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in breakfast.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Patrick Koperwas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Breakfast 6 | 7 | [Breakfast](http://breakfast.devlocker.io/) integrates modern Javascript 8 | tooling into your Rails project. Powered by [Brunch.io](http://brunch.io). 9 | 10 | Get support for ES6 syntax & modules, live reload for CSS, JS, & HTML, and Yarn 11 | support. Be up and running on the latest frontend framework in minutes. 12 | 13 | ### Installation & Usage 14 | 15 | See the official docs at 16 | [http://breakfast.devlocker.io](http://breakfast.devlocker.io). 17 | 18 | View updates in the [CHANGELOG](https://github.com/devlocker/breakfast/blob/master/CHANGELOG.md) 19 | 20 | ### Latest Patch `0.6.6` 21 | 22 | #### Fixed 23 | 24 | - Support Rails 6 by removing constraint on ActionCable 25 | 26 | [@mattr](https://github.com/devlocker/breakfast/pull/32) 27 | 28 | ### Latest Release `0.6.0` 29 | 30 | #### Fixed 31 | 32 | - Puma hanging in clustered mode. Breakfast would fail to cleanly exit on Puma 33 | exit, causing the server to hang indefinitely. 34 | - Bumped Rails version dependency, can be used with Rails 5.0 and greater. 35 | (Allows usage with Rails 5.1) 36 | 37 | #### Removed 38 | 39 | - Capistrano rake tasks. Previous behavior has been included into the Rails 40 | asset:precompile task. Using the standard [Capistrano Rails](https://github.com/capistrano/rails) 41 | gem is all that required now. 42 | - Need for a custom Heroku buildpack. 43 | 44 | ### Upgrading 45 | 46 | #### Upgrading to `0.6.0` from `0.5.x` 47 | 48 | - Update gem with `bundle update breakfast` 49 | - Update the JS package with `yarn upgrade breakfast-rails` 50 | - If deploying with Capistrano, remove `require "breakfast/capistrano"` from 51 | your `Capfile`. Remove any custom Breakfast settings from `config/deploy.rb`. 52 | Ensure that you are using [Capistrano Rails](https://github.com/capistrano/rails) 53 | and have `require 'capistrano/rails'` or `require 'capistrano/rails/assets'` 54 | in your `Capfile`. 55 | - If deploying with Heroku, run the following commands: 56 | 1. heroku buildpacks:clear 57 | 2. heroku buildpacks:set heroku/nodejs --index 1 58 | 3. heroku buildpacks:set heroku/ruby --index 2 59 | 60 | _Note_ If you are deploying with Capistrano then Yarn is expected to be 61 | installed on the hosts your are deploying to. 62 | 63 | ### Changes 64 | 65 | See list of changes between versions in the CHANGELOG 66 | 67 | ### Contributing 68 | 69 | Bug reports and pull requests are welcome on GitHub at 70 | https://github.com/devlocker/breakfast. 71 | 72 | ### License 73 | 74 | The gem is available as open source under the terms of the [MIT 75 | License](http://opensource.org/licenses/MIT). 76 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "breakfast" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /breakfast.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "breakfast/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "breakfast" 8 | spec.version = Breakfast::VERSION 9 | spec.authors = ["Patrick Koperwas"] 10 | spec.email = ["patrick@devlocker.io"] 11 | 12 | spec.summary = %q{Integrates Brunch into Rails} 13 | spec.description = %q{Replace the asset pipeline with Brunch. Get CSS, JS and HTML live-reloading out of the box. Full ES6 support with require.} 14 | spec.homepage = "https://github.com/devlocker/breakfast" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_development_dependency "bundler", "~> 1.12" 23 | spec.add_development_dependency "rake", "~> 10.0" 24 | spec.add_development_dependency "rspec", "~> 3.0" 25 | spec.add_dependency "rails", ">= 5.0" 26 | spec.add_dependency "actioncable", ">= 5.0" 27 | spec.add_dependency "listen", ">= 3.0" 28 | end 29 | -------------------------------------------------------------------------------- /lib/breakfast.rb: -------------------------------------------------------------------------------- 1 | require "breakfast/version" 2 | 3 | require "breakfast/brunch_watcher" 4 | require "breakfast/compilation_listener" 5 | require "breakfast/live_reload_channel" 6 | require "breakfast/manifest" 7 | require "breakfast/helper" 8 | require "breakfast/local_environment" 9 | require "breakfast/status_channel" 10 | 11 | module Breakfast 12 | STATUS_CHANNEL = "breakfast_status".freeze 13 | RELOAD_CHANNEL = "breakfast_live_reload".freeze 14 | end 15 | 16 | require "breakfast/railtie" if defined?(::Rails) 17 | -------------------------------------------------------------------------------- /lib/breakfast/brunch_watcher.rb: -------------------------------------------------------------------------------- 1 | require "pty" 2 | 3 | module Breakfast 4 | class BrunchWatcher 5 | BRUNCH_COMMAND = "./node_modules/brunch/bin/brunch watch".freeze 6 | 7 | attr_accessor :pid 8 | attr_reader :log 9 | def initialize(log:) 10 | @log = log 11 | end 12 | 13 | def run 14 | out, writer, self.pid = PTY.spawn(BRUNCH_COMMAND) 15 | writer.close 16 | 17 | Process.detach(pid) 18 | 19 | begin 20 | loop do 21 | output = out.readpartial(64.kilobytes).strip 22 | log.debug output 23 | 24 | output = output.gsub(/\e\[([;\d]+)?m/, "") 25 | case output 26 | when /compiled/ 27 | broadcast(status: "success", message: output.split("info: ").last) 28 | when /error/ 29 | broadcast(status: "error", message: output.split("error: ").last) 30 | end 31 | end 32 | rescue EOFError, Errno::EIO 33 | log.fatal "[BREAKFAST] Watcher died unexpectedly. Restart Rails Server" 34 | broadcast( 35 | status: "error", 36 | message: "Watcher died unexpectedly. Restart Rails server" 37 | ) 38 | end 39 | end 40 | 41 | def terminate 42 | Process.kill("TERM", @pid) 43 | rescue Errno::ESRCH 44 | # NOOP. Process exited cleanly or already terminated. Don't print 45 | # exception to STDOUT 46 | end 47 | 48 | private 49 | 50 | def broadcast(status:, message:) 51 | ActionCable.server.broadcast(STATUS_CHANNEL, { 52 | status: status, 53 | message: message 54 | }) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/breakfast/compilation_listener.rb: -------------------------------------------------------------------------------- 1 | module Breakfast 2 | class CompilationListener 3 | ASSET_EXTENSIONS = ["css", "js"].freeze 4 | SOURCE_CODE_EXTENSIONS = ["rb", "html", "haml", "slim"].freeze 5 | 6 | def self.start(asset_output_folder:, source_code_folders:) 7 | asset_listener = 8 | ::Listen.to(asset_output_folder) do |modified, added, removed| 9 | files = modified + added + removed 10 | 11 | ASSET_EXTENSIONS.each do |extension| 12 | if files.any? { |file| file.match(/\.#{extension}/) } 13 | broadcast(Breakfast::RELOAD_CHANNEL, { extension: extension }) 14 | end 15 | end 16 | end 17 | 18 | rails_listener = 19 | ::Listen.to(*source_code_folders) do |modified, added, removed| 20 | files = modified + added + removed 21 | 22 | SOURCE_CODE_EXTENSIONS.each do |extension| 23 | matched = files.select { |file| file.match(/\.#{extension}/) } 24 | if matched.present? 25 | broadcast(Breakfast::RELOAD_CHANNEL, { extension: extension }) 26 | 27 | file_names = matched 28 | .map { |file| file.split("/").last } 29 | .join(", ") 30 | .truncate(60) 31 | 32 | broadcast(Breakfast::STATUS_CHANNEL, { 33 | status: "success", 34 | message: "saved: #{file_names}", 35 | extension: extension 36 | }) 37 | end 38 | end 39 | end 40 | 41 | asset_listener.start 42 | rails_listener.start 43 | end 44 | 45 | def self.broadcast(channel, data) 46 | ActionCable.server.broadcast(channel, data) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/breakfast/helper.rb: -------------------------------------------------------------------------------- 1 | module Breakfast 2 | module Helper 3 | def breakfast_autoreload_tag 4 | if ::Rails.configuration.breakfast.environments.include?(::Rails.env) 5 | content_tag :script do 6 | <<-SCRIPT.html_safe 7 | require("breakfast-rails").init({ 8 | host: "#{request.host}", 9 | port: #{request.port}, 10 | strategies: { 11 | js: "#{::Rails.configuration.breakfast.js_reload_strategy}", 12 | css: "#{::Rails.configuration.breakfast.css_reload_strategy}", 13 | html: "#{::Rails.configuration.breakfast.html_reload_strategy}", 14 | rb: "#{::Rails.configuration.breakfast.ruby_reload_strategy}" 15 | }, 16 | statusBarLocation: "#{::Rails.configuration.breakfast.status_bar_location}" 17 | }); 18 | SCRIPT 19 | end 20 | end 21 | end 22 | 23 | include ActionView::Helpers::AssetUrlHelper 24 | include ActionView::Helpers::AssetTagHelper 25 | 26 | def compute_asset_path(path, options = {}) 27 | if ::Rails.configuration.breakfast.digest && ::Rails.configuration.breakfast.manifest.asset(path) 28 | path = ::Rails.configuration.breakfast.manifest.asset(path) 29 | end 30 | 31 | super(path, options) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/breakfast/live_reload_channel.rb: -------------------------------------------------------------------------------- 1 | require "rails" 2 | 3 | module Breakfast 4 | class LiveReloadChannel < ::ActionCable::Channel::Base 5 | def subscribed 6 | stream_from "breakfast_live_reload" 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/breakfast/local_environment.rb: -------------------------------------------------------------------------------- 1 | module Breakfast 2 | class LocalEnvironment 3 | def running_server? 4 | possible_servers = %w[ 5 | rails 6 | puma 7 | passenger 8 | unicorn 9 | mongrel 10 | webrick 11 | rainbows 12 | ] 13 | 14 | possible_servers.any? do |server| 15 | send "detect_#{server}" 16 | end 17 | end 18 | 19 | private 20 | 21 | def detect_rails 22 | defined?(::Rails::Server) 23 | end 24 | 25 | def detect_puma 26 | defined?(::Puma) && File.basename($0) == "puma" 27 | end 28 | 29 | def detect_passenger 30 | defined?(::PhusionPassenger) 31 | end 32 | 33 | def detect_thin 34 | defined?(::Thin) && defined?(::Thin::Server) 35 | end 36 | 37 | def detect_unicorn 38 | defined?(::Unicorn) && defined?(::Unicorn::HttpServer) 39 | end 40 | 41 | def detect_mongrel 42 | defined?(::Mongrel) && defined?(::Mongrel::HttpServer) 43 | end 44 | 45 | def detect_webrick 46 | defined?(::WEBrick) && defined?(::WEBrick::VERSION) 47 | end 48 | 49 | def detect_rainbows 50 | defined?(::Rainbows) && defined?(::Rainbows::HttpServer) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/breakfast/manifest.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "digest" 3 | require "fileutils" 4 | 5 | module Breakfast 6 | class Manifest 7 | MANIFEST_REGEX = /^\.breakfast-manifest-[0-9a-f]{32}.json$/ 8 | SPROCKETS_MANIFEST_REGEX = /^\.sprockets-manifest-[0-9a-f]{32}.json$/ 9 | FINGERPRINT_REGEX = /-[0-9a-f]{32}\./ 10 | 11 | attr_reader :base_dir, :manifest_path, :cache 12 | def initialize(base_dir:) 13 | FileUtils.mkdir_p(base_dir) 14 | 15 | @base_dir = Pathname.new(base_dir) 16 | @manifest_path = find_manifest_or_create 17 | @cache = update_cache! 18 | end 19 | 20 | def asset(path) 21 | cache[path] 22 | end 23 | 24 | # The #digest! method will run through all of the compiled assets and 25 | # create a copy of each asset with a digest fingerprint. This fingerprint 26 | # will change whenever the file contents change. This allows us to use HTTP 27 | # headers to cache these assets as we will be able to reliably know when 28 | # they change. 29 | # 30 | # These fingerprinted files are copies of the original. The originals are 31 | # not removed and still available should the need arise to serve a 32 | # non-fingerprinted asset. 33 | # 34 | # Example manifest: 35 | # { 36 | # app.js => app-76c6ee161ba431e823301567b175acda.js, 37 | # images/logo.png => images/logo-869269cdf1773ff0dec91bafb37310ea.png, 38 | # } 39 | # 40 | # Resulting File Structure: 41 | # - / 42 | # - app.js 43 | # - app-76c6ee161ba431e823301567b175acda.js 44 | # - images/ 45 | # - logo.png 46 | # - logo-869269cdf1773ff0dec91bafb37310ea.png 47 | def digest! 48 | assets = asset_paths.map do |path| 49 | digest = Digest::MD5.new 50 | digest.update(File.read("#{base_dir}/#{path}")) 51 | 52 | digest_path = "#{path.sub_ext('')}-#{digest.hexdigest}#{File.extname(path)}" 53 | 54 | FileUtils.cp("#{base_dir}/#{path}", "#{base_dir}/#{digest_path}") 55 | 56 | [path, digest_path] 57 | end 58 | 59 | File.open(manifest_path, "w") do |manifest| 60 | manifest.write(assets.to_h.to_json) 61 | end 62 | 63 | update_cache! 64 | end 65 | 66 | # Remove any files not directly referenced by the manifest. 67 | def clean! 68 | files_to_keep = cache.keys.concat(cache.values) 69 | 70 | if (sprockets_manifest = Dir.entries("#{base_dir}").detect { |entry| entry =~ SPROCKETS_MANIFEST_REGEX }) 71 | files_to_keep.concat(JSON.parse(File.read("#{base_dir}/#{sprockets_manifest}")).fetch("files", {}).keys) 72 | end 73 | 74 | Dir["#{base_dir}/**/*"].each do |path| 75 | next if File.directory?(path) || files_to_keep.include?(Pathname(path).relative_path_from(base_dir).to_s) 76 | 77 | FileUtils.rm(path) 78 | end 79 | end 80 | 81 | # Remove manifest, any fingerprinted files. 82 | def nuke! 83 | Dir["#{base_dir}/**/*"] 84 | .select { |path| path =~ FINGERPRINT_REGEX } 85 | .each { |file| FileUtils.rm(file) } 86 | 87 | FileUtils.rm(manifest_path) 88 | end 89 | 90 | private 91 | 92 | def update_cache! 93 | @cache = JSON.parse(File.read(manifest_path)) 94 | end 95 | 96 | # Creates a or finds a manifest file in a given directory. The manifest 97 | # file is is prefixed with a dot ('.') and given a random string to ensure 98 | # the file is not served or easily discoverable. 99 | def find_manifest_or_create 100 | if (manifest = Dir.entries("#{base_dir}").detect { |entry| entry =~ MANIFEST_REGEX }) 101 | "#{base_dir}/#{manifest}" 102 | else 103 | manifest = "#{base_dir}/.breakfast-manifest-#{SecureRandom.hex(16)}.json" 104 | File.open(manifest, "w") { |manifest| manifest.write({}.to_json) } 105 | manifest 106 | end 107 | end 108 | 109 | def asset_paths 110 | Dir["#{base_dir}/**/*"] 111 | .reject { |path| File.directory?(path) || path =~ FINGERPRINT_REGEX } 112 | .map { |file| Pathname(file).relative_path_from(base_dir) } 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/breakfast/railtie.rb: -------------------------------------------------------------------------------- 1 | require "rails" 2 | require "fileutils" 3 | require "listen" 4 | 5 | module Breakfast 6 | class Railtie < ::Rails::Railtie 7 | config.breakfast = ActiveSupport::OrderedOptions.new 8 | 9 | config.before_configuration do |app| 10 | config.breakfast.html_reload_strategy = :turbolinks 11 | config.breakfast.js_reload_strategy = :page 12 | config.breakfast.css_reload_strategy = :hot 13 | config.breakfast.ruby_reload_strategy = :off 14 | 15 | config.breakfast.asset_output_folder = ::Rails.root.join("public", "assets") 16 | config.breakfast.source_code_folders = [::Rails.root.join("app")] 17 | config.breakfast.environments = %w(development) 18 | config.breakfast.status_bar_location = :bottom 19 | config.breakfast.digest = !(::Rails.env.development? || ::Rails.env.test?) 20 | end 21 | 22 | initializer "breakfast.setup_view_helpers" do |app| 23 | ActiveSupport.on_load(:action_view) do 24 | include ::Breakfast::Helper 25 | end 26 | end 27 | 28 | rake_tasks do 29 | load "tasks/breakfast.rake" 30 | end 31 | 32 | config.after_initialize do |app| 33 | if config.breakfast.digest 34 | config.breakfast.manifest = Breakfast::Manifest.new(base_dir: config.breakfast.asset_output_folder) 35 | end 36 | 37 | if config.breakfast.environments.include?(::Rails.env) && Breakfast::LocalEnvironment.new.running_server? 38 | 39 | # Ensure public/assets directory exists 40 | FileUtils.mkdir_p(::Rails.root.join("public", "assets")) 41 | 42 | # Start Brunch Process 43 | @brunch = Breakfast::BrunchWatcher.new(log: ::Rails.logger) 44 | Thread.start do 45 | @brunch.run 46 | end 47 | 48 | at_exit do 49 | @brunch.terminate 50 | end 51 | 52 | # Setup file listeners 53 | Breakfast::CompilationListener.start( 54 | asset_output_folder: config.breakfast.asset_output_folder, 55 | source_code_folders: config.breakfast.source_code_folders 56 | ) 57 | end 58 | end 59 | 60 | ActionView::Helpers::AssetUrlHelper::ASSET_PUBLIC_DIRECTORIES[:javascript] = "/assets" 61 | ActionView::Helpers::AssetUrlHelper::ASSET_PUBLIC_DIRECTORIES[:image] = "/assets" 62 | ActionView::Helpers::AssetUrlHelper::ASSET_PUBLIC_DIRECTORIES[:stylesheet] = "/assets" 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/breakfast/status_channel.rb: -------------------------------------------------------------------------------- 1 | require "rails" 2 | 3 | module Breakfast 4 | class StatusChannel < ::ActionCable::Channel::Base 5 | def subscribed 6 | stream_from "breakfast_status" 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/breakfast/version.rb: -------------------------------------------------------------------------------- 1 | module Breakfast 2 | VERSION = "0.6.6" 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/breakfast/install_generator.rb: -------------------------------------------------------------------------------- 1 | require "rails/generators" 2 | 3 | module Breakfast 4 | module Generators 5 | class InstallGenerator < ::Rails::Generators::Base 6 | source_root File.expand_path("../templates", __FILE__) 7 | NODE_VERSION = Gem::Version.new("4.1.0") 8 | 9 | def install 10 | if node_prerequisites_installed? 11 | create_brunch_config 12 | create_package_json if using_rails_5_dot_0? 13 | install_required_packages 14 | create_directory_structure 15 | create_app_js_file 16 | create_app_scss_file 17 | create_gitkeep_files 18 | add_node_modules_to_gitignore 19 | 20 | puts <<-SUCCESS.strip_heredoc 21 | 22 | ---> BREAKFAST INSTALLED SUCCESSFULLY 23 | ---> See https://github.com/devlocker/breakfast for documentation and examples. 24 | 25 | SUCCESS 26 | else 27 | puts <<-ERROR.strip_heredoc 28 | 29 | ---> ERROR - MISSING NODE & YARN 30 | 31 | ---> Node version >= #{NODE_VERSION} & yarn are required to run Breakfast. 32 | ---> Please install them before attempting to continue. 33 | ---> https://nodejs.org 34 | ---> https://yarnpkg.com/docs/install/ 35 | 36 | ERROR 37 | end 38 | end 39 | 40 | private 41 | 42 | def node_prerequisites_installed? 43 | `which node`.present? && `which yarn`.present? && node_versions_are_satisfactory? 44 | end 45 | 46 | def node_versions_are_satisfactory? 47 | installed_node_version >= NODE_VERSION 48 | end 49 | 50 | def installed_node_version 51 | Gem::Version.new(`node -v`.tr("v", "")) 52 | end 53 | 54 | def create_brunch_config 55 | copy_file "brunch-config.js", "brunch-config.js" 56 | end 57 | 58 | def create_package_json 59 | copy_file "package.json", "package.json" 60 | end 61 | 62 | def install_required_packages 63 | packages = %w( 64 | actioncable 65 | babel 66 | babel-brunch 67 | breakfast-rails 68 | brunch 69 | clean-css-brunch 70 | jquery 71 | jquery-ujs 72 | sass-brunch 73 | turbolinks 74 | uglify-js-brunch 75 | ) 76 | run "yarn add #{packages.join(' ')}" 77 | end 78 | 79 | def create_directory_structure 80 | empty_directory "app/frontend/css" 81 | empty_directory "app/frontend/images" 82 | empty_directory "app/frontend/js" 83 | empty_directory "app/frontend/vendor" 84 | end 85 | 86 | def create_app_js_file 87 | copy_file "app.js", "app/frontend/js/app.js" 88 | end 89 | 90 | def create_app_scss_file 91 | copy_file "app.scss", "app/frontend/css/app.scss" 92 | end 93 | 94 | def create_gitkeep_files 95 | create_file "app/frontend/images/.gitkeep" 96 | create_file "app/frontend/vendor/.gitkeep" 97 | end 98 | 99 | def add_node_modules_to_gitignore 100 | ignore = <<-IGNORE.strip_heredoc 101 | # Added by Breakfast Gem 102 | yarn-error.log 103 | npm-debug.log 104 | node_modules/* 105 | public/assets/* 106 | IGNORE 107 | 108 | append_to_file(".gitignore", ignore) 109 | end 110 | 111 | def using_rails_5_dot_0? 112 | Gem::Version.new(::Rails.version) < Gem::Version.new("5.1") 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/generators/breakfast/templates/app.js: -------------------------------------------------------------------------------- 1 | // Turbolinks - To disable remove the next two lines. 2 | // https://github.com/turbolinks/turbolinks 3 | import Turbolinks from 'turbolinks'; 4 | Turbolinks.start(); 5 | 6 | // Require https://github.com/rails/jquery-ujs 7 | import 'jquery-ujs'; 8 | 9 | const App = { 10 | init() { 11 | } 12 | } 13 | 14 | module.exports = App; 15 | -------------------------------------------------------------------------------- /lib/generators/breakfast/templates/app.scss: -------------------------------------------------------------------------------- 1 | // Your applications SCSS file 2 | body { 3 | } 4 | -------------------------------------------------------------------------------- /lib/generators/breakfast/templates/brunch-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | files: { 3 | javascripts: { 4 | joinTo: 'app.js' 5 | }, 6 | stylesheets: { 7 | joinTo: 'app.css' 8 | }, 9 | templates: { 10 | joinTo: 'app.js' 11 | } 12 | }, 13 | 14 | plugins: { 15 | babel: { 16 | presets: ['es2015'] 17 | }, 18 | }, 19 | 20 | paths: { 21 | watched: [ 22 | 'app/frontend', 23 | ], 24 | 25 | public: 'public/assets' 26 | }, 27 | 28 | conventions: { 29 | assets: /^(app\/frontend\/images)/ 30 | }, 31 | 32 | npm: { 33 | globals: { 34 | $: 'jquery', 35 | jQuery: 'jquery', 36 | breakfast: 'breakfast-rails' 37 | } 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /lib/generators/breakfast/templates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": {}, 3 | "dependencies": { 4 | }, 5 | "devDependencies": { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/tasks/breakfast.rake: -------------------------------------------------------------------------------- 1 | require "rake" 2 | require "breakfast" 3 | 4 | namespace :breakfast do 5 | namespace :assets do 6 | desc "Prepare assets and digests for production deploy" 7 | task compile: [:environment] do 8 | Rake::Task["breakfast:assets:build_production"].execute 9 | Rake::Task["breakfast:assets:digest"].execute 10 | Rake::Task["breakfast:assets:clean"].execute 11 | end 12 | 13 | desc "Build assets for production" 14 | task build_production: :environment do 15 | Breakfast.call_system "NODE_ENV=production ./node_modules/brunch/bin/brunch build --production" 16 | end 17 | 18 | desc "Build assets" 19 | task build: :environment do 20 | Breakfast.call_system "./node_modules/brunch/bin/brunch build" 21 | end 22 | 23 | desc "Add a digest to non-fingerprinted assets" 24 | task digest: :environment do 25 | if ::Rails.configuration.breakfast.manifest 26 | ::Rails.configuration.breakfast.manifest.digest! 27 | else 28 | raise Breakfast::ManifestDisabledError 29 | end 30 | end 31 | 32 | desc "Remove out of date assets" 33 | task clean: :environment do 34 | if ::Rails.configuration.breakfast.manifest 35 | ::Rails.configuration.breakfast.manifest.clean! 36 | else 37 | raise Breakfast::ManifestDisabledError 38 | end 39 | end 40 | 41 | desc "Remove manifest and fingerprinted assets" 42 | task nuke: :environment do 43 | if ::Rails.configuration.breakfast.manifest 44 | ::Rails.configuration.breakfast.manifest.nuke! 45 | else 46 | raise Breakfast::ManifestDisabledError 47 | end 48 | end 49 | end 50 | 51 | namespace :yarn do 52 | desc "Install package.json dependencies with Yarn" 53 | task :install do 54 | Breakfast.call_system "yarn" 55 | end 56 | end 57 | end 58 | 59 | if Rake::Task.task_defined?("assets:precompile") 60 | Rake::Task["assets:precompile"].enhance do 61 | unless File.exist?("./bin/yarn") && Rake::Task.task_defined?("yarn:install") 62 | # Rails 5.1 includes a yarn install command - don't yarn install twice. 63 | Rake::Task["breakfast:yarn:install"].invoke 64 | end 65 | Rake::Task["breakfast:assets:compile"].invoke 66 | end 67 | else 68 | Rake::Task.define_task( 69 | "assets:precompile" => ["breakfast:yarn:install", "breakfast:assets:compile"] 70 | ) 71 | end 72 | 73 | module Breakfast 74 | class ManifestDisabledError < StandardError 75 | def initialize 76 | super( 77 | <<~ERROR 78 | ::Rails.configuration.breakfast.manifest is set to false. 79 | Enable it by adding the following in your environment file: 80 | 81 | config.breakfast.manifest.digest = true 82 | 83 | *Note* by default digest is set to false in development and test enviornments. 84 | 85 | ERROR 86 | ) 87 | end 88 | end 89 | 90 | SystemCallError = Class.new(StandardError) 91 | 92 | def self.call_system(cmd) 93 | raise SystemCallError, "Failed to execute system command: \"#{cmd}\"" unless system(cmd) 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /node_package/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true 6 | }, 7 | "rules": { 8 | "block-spacing": [2, "always"], 9 | "brace-style": [2, "1tbs", {"allowSingleLine": true}], 10 | "comma-style": [2, "last"], 11 | "func-style": [2, "expression"], 12 | "semi": [2, "always"], 13 | "quotes": [2, "single", "avoid-escape"], 14 | "indent": [2, 2, {"SwitchCase": 1}], 15 | "dot-location": [2, "property"], 16 | "camelcase": [1, {"properties": "always"}], 17 | "comma-spacing": [2, {"before": false, "after": true}], 18 | "comma-dangle": [2, "never"], 19 | "semi-spacing": [2, {"before": false, "after": true}], 20 | "curly": [2, "multi-line", "consistent"], 21 | "no-debugger": 2, 22 | "no-dupe-args": 2, 23 | "no-dupe-keys": 2, 24 | "no-duplicate-case": 2, 25 | "no-empty": 2, 26 | "no-ex-assign": 2, 27 | "no-extra-semi": 2, 28 | "no-func-assign": 2, 29 | "no-irregular-whitespace": 2, 30 | "no-sparse-arrays": 2, 31 | "no-unexpected-multiline": 2, 32 | "no-unreachable": 2, 33 | "no-unused-vars": [2, {"varsIgnorePattern": "ignored"}], 34 | "valid-typeof": 2, 35 | "eqeqeq": [2, "allow-null"], 36 | "no-array-constructor": 2, 37 | "no-caller": 2, 38 | "no-eval": 2, 39 | "no-extend-native": 2, 40 | "no-extra-bind": 2, 41 | "no-fallthrough": 2, 42 | "no-labels": 2, 43 | "no-iterator": 2, 44 | "no-magic-numbers": [1, {"ignore": [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8]}], 45 | "no-multi-spaces": 2, 46 | "no-native-reassign": 2, 47 | "no-new-func": 2, 48 | "no-new-wrappers": 2, 49 | "no-new": 2, 50 | "no-octal-escape": 2, 51 | "no-octal": 2, 52 | "no-redeclare": 2, 53 | "no-self-compare": 2, 54 | "no-sequences": 2, 55 | "no-unused-expressions": 2, 56 | "no-useless-call": 2, 57 | "no-warning-comments": [1, {"terms": ["todo", "fixme", "xxx"], "location": "start"}], 58 | "no-with": 2, 59 | "new-parens": 2, 60 | "wrap-iife": [2, "inside"], 61 | "no-catch-shadow": 2, 62 | "no-delete-var": 2, 63 | "no-shadow-restricted-names": 2, 64 | "no-undef": 2, 65 | "callback-return": 2, 66 | "handle-callback-err": 2, 67 | "no-path-concat": 2, 68 | "array-bracket-spacing": 2, 69 | "eol-last": 2, 70 | "no-multiple-empty-lines": [2, {"max": 2}], 71 | "no-spaced-func": 2, 72 | "no-trailing-spaces": 2, 73 | "no-unneeded-ternary": 2, 74 | "keyword-spacing": 2, 75 | "space-before-blocks": 2, 76 | "space-before-function-paren": [2, "never"], 77 | "space-in-parens": 2, 78 | "space-unary-ops": [2, {"words": true, "nonwords": false}], 79 | "arrow-spacing": [2, {"before": true, "after": true}], 80 | "prefer-arrow-callback": 2, 81 | "prefer-template": 0, 82 | "prefer-const": 2 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /node_package/.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | -------------------------------------------------------------------------------- /node_package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "breakfast-rails", 3 | "version": "0.6.5", 4 | "description": "Assets for the Breakfast Gem", 5 | "main": "./lib/breakfast-rails.js", 6 | "scripts": { 7 | "test": "eslint index.js src", 8 | "prepublish": "node_modules/babel-cli/bin/babel.js src --out-dir lib" 9 | }, 10 | "author": "Patrick Koperwas", 11 | "license": "MIT", 12 | "dependencies": { 13 | "actioncable": "^5.0.0" 14 | }, 15 | "devDependencies": { 16 | "babel-cli": "^6.10.1", 17 | "babel-core": "^6.10.4", 18 | "babel-preset-es2015": "^6.9.0", 19 | "eslint": "^2.4.0" 20 | }, 21 | "babel": { 22 | "presets": ["es2015"] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /node_package/src/breakfast-rails.js: -------------------------------------------------------------------------------- 1 | const LiveReloader = require('./live-reload'); 2 | const StatusBar = require('./status-bar'); 3 | const Settings = require('./settings'); 4 | 5 | const BreakfastRails = { 6 | init(options = {}) { 7 | window.Breakfast = (window.Breakfast || {}); 8 | 9 | const settings = new Settings(options); 10 | const liveReloader = new LiveReloader(settings); 11 | const statusBar = new StatusBar(settings); 12 | 13 | liveReloader.init(); 14 | statusBar.init(); 15 | } 16 | }; 17 | 18 | module.exports = BreakfastRails; 19 | 20 | -------------------------------------------------------------------------------- /node_package/src/live-reload.js: -------------------------------------------------------------------------------- 1 | const RELOAD_CHANNEL = 'Breakfast::LiveReloadChannel'; 2 | 3 | class LiveReloader { 4 | constructor(settings) { 5 | this.settings = settings; 6 | } 7 | 8 | buildFreshUrl(url) { 9 | const date = Math.round(Date.now() / 1000).toString(); 10 | url = url.replace(/(\&|\\?)version=\d*/, ''); 11 | 12 | return (`${url}${(url.indexOf('?') >= 0 ? '&' : '?')}version=${date}`); 13 | } 14 | 15 | cssReload(strategy) { 16 | switch (strategy) { 17 | case 'hot': 18 | const reloadableLinkElements = window.top.document.querySelectorAll( 19 | 'link[rel=stylesheet]:not([data-no-reload]):not([data-pending-removal])' 20 | ); 21 | 22 | [].slice 23 | .call(reloadableLinkElements) 24 | .filter(link => link.href) 25 | .forEach(link => link.href = this.buildFreshUrl(link.href)); 26 | 27 | // Repaint 28 | const browser = navigator.userAgent.toLowerCase(); 29 | 30 | if (browser.indexOf('chrome') > -1) { 31 | setTimeout(() => { document.body.offsetHeight; }, 25); 32 | } 33 | break; 34 | case 'page': 35 | window.top.location.reload(); 36 | break; 37 | case 'off': 38 | break; 39 | } 40 | } 41 | 42 | jsReload(strategy) { 43 | switch (strategy) { 44 | case 'page': 45 | window.top.location.reload(); 46 | break; 47 | case 'off': 48 | break; 49 | } 50 | } 51 | 52 | htmlReload(strategy) { 53 | switch (strategy) { 54 | case 'turbolinks': 55 | this.reloadTurbolinks(); 56 | break; 57 | case 'wiselinks': 58 | this.reloadWiselinks(); 59 | break; 60 | case 'page': 61 | window.top.location.reload(); 62 | break; 63 | case 'off': 64 | break; 65 | } 66 | } 67 | 68 | rubyReload(strategy) { 69 | switch (strategy) { 70 | case 'turbolinks': 71 | this.reloadTurbolinks(); 72 | break; 73 | case 'wiselinks': 74 | this.reloadWiselinks(); 75 | break; 76 | case 'page': 77 | window.top.location.reload(); 78 | break; 79 | case 'off': 80 | break; 81 | } 82 | } 83 | 84 | reloadTurbolinks() { 85 | const location = window.top.location; 86 | 87 | if (this.settings.turbolinksEnabled() && !this.onErrorPage()) { 88 | Turbolinks.visit(location); 89 | } else { 90 | location.reload(); 91 | } 92 | } 93 | 94 | reloadWiselinks() { 95 | if (this.settings.wiselinksEnabled() && !this.onErrorPage()) { 96 | wiselinks.reload(); 97 | } else { 98 | window.top.location.reload(); 99 | } 100 | } 101 | // If user is on an error page and they fix the error and re-render using 102 | // turbolinks than the CSS from the Rails error page will hang around. Will 103 | // initiate a full refresh to get rid of it. 104 | onErrorPage() { 105 | return (document.title.indexOf('Exception caught') !== -1); 106 | } 107 | 108 | init() { 109 | const reloaders = { 110 | js: this.jsReload.bind(this), 111 | css: this.cssReload.bind(this), 112 | html: this.htmlReload.bind(this), 113 | slim: this.htmlReload.bind(this), 114 | haml: this.htmlReload.bind(this), 115 | rb: this.rubyReload.bind(this) 116 | }; 117 | 118 | document.addEventListener('DOMContentLoaded', () => { 119 | this.settings.cable.subscriptions.create(RELOAD_CHANNEL, { 120 | received: (data) => { 121 | const reloader = reloaders[data.extension]; 122 | reloader(this.settings.strategies[data.extension]); 123 | } 124 | }); 125 | }); 126 | } 127 | } 128 | 129 | module.exports = LiveReloader; 130 | -------------------------------------------------------------------------------- /node_package/src/settings.js: -------------------------------------------------------------------------------- 1 | const LOCAL_STORAGE_KEY = 'breakfast-rails-settings'; 2 | const ActionCable = require('actioncable'); 3 | 4 | class Settings { 5 | constructor(options = {}) { 6 | this.options = options; 7 | this.host = options.host; 8 | this.port = options.port; 9 | this.strategies = this.determineStrategies(); 10 | this.statusBarLocation = options.statusBarLocation; 11 | this.cable = ActionCable.createConsumer(`ws://${this.host}:${this.port}/cable`); 12 | this.log = (this.storedSettings().log || {}); 13 | 14 | this.save(); 15 | } 16 | 17 | save() { 18 | localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify({ 19 | host: this.host, 20 | port: this.port, 21 | strategies: this.strategies, 22 | log: this.log 23 | })); 24 | } 25 | 26 | determineStrategies() { 27 | const defaults = this.options.strategies || {}; 28 | const existing = this.storedSettings().strategies || {}; 29 | 30 | return (Object.assign(defaults, existing)); 31 | } 32 | 33 | updateStrategies(strategy) { 34 | this.strategies = Object.assign(this.strategies, strategy); 35 | this.save(); 36 | } 37 | 38 | updateLog(log) { 39 | this.log = log; 40 | this.save(); 41 | } 42 | 43 | storedSettings() { 44 | return (JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || {}); 45 | } 46 | 47 | turbolinksEnabled() { 48 | return typeof Turbolinks !== 'undefined'; 49 | } 50 | 51 | wiselinksEnabled() { 52 | return typeof Wiselinks !== 'undefined'; 53 | } 54 | } 55 | 56 | module.exports = Settings; 57 | -------------------------------------------------------------------------------- /node_package/src/status-bar.js: -------------------------------------------------------------------------------- 1 | const STATUS_CHANNEL = 'Breakfast::StatusChannel'; 2 | 3 | class StatusBar { 4 | constructor(settings = {}) { 5 | this.settings = settings; 6 | 7 | this.settings.cable.subscriptions.create(STATUS_CHANNEL, { 8 | connected: () => { 9 | this.write(this.settings.log.message, this.settings.log.status); 10 | }, 11 | received: (log) => { 12 | if (this.settings.strategies[log.extension] !== 'off') { 13 | this.settings.updateLog(log); 14 | this.write(log.message, log.status); 15 | } 16 | }, 17 | disconnected: () => { 18 | this.write('Disconnected from server...', 'error'); 19 | } 20 | }); 21 | } 22 | 23 | init() { 24 | let eventName; 25 | 26 | if (this.settings.turbolinksEnabled()) { 27 | eventName = 'turbolinks:load'; 28 | } else if (this.settings.wiselinksEnabled()) { 29 | eventName = 'page:done'; 30 | } else { 31 | eventName = 'DOMContentLoaded'; 32 | } 33 | 34 | document.addEventListener(eventName, () => { 35 | this.render(); 36 | this.write(this.settings.log.message, this.settings.log.status); 37 | }); 38 | 39 | window.Breakfast.StatusBar = this; 40 | } 41 | 42 | write(message, status) { 43 | const log = document.getElementById('breakfast-message-log'); 44 | if (log) { 45 | log.innerHTML = message; 46 | log.className = `breakfast-message-log-${ status }`; 47 | } 48 | } 49 | 50 | handleClick(option) { 51 | this.settings.updateStrategies(option); 52 | const reloaders = document.getElementById('breakfast-reloaders'); 53 | reloaders.innerHTML = this.renderReloaders(); 54 | } 55 | 56 | render() { 57 | const statusBar = document.getElementById('breakfast-status-bar'); 58 | 59 | if (statusBar) { document.body.removeChild(statusBar); } 60 | 61 | const sb = document.createElement('DIV'); 62 | sb.setAttribute('class', 'breakfast-status-bar'); 63 | sb.setAttribute('id', 'breakfast-status-bar'); 64 | 65 | sb.innerHTML = ` 66 | ${ this.stylesheet() } 67 |
68 | ${ this.renderReloaders() } 69 |
70 |
71 |
72 | `; 73 | 74 | document.body.appendChild(sb); 75 | } 76 | 77 | renderReloaders() { 78 | return (` 79 |
80 | js: ${ this.settings.strategies.js } 81 |
82 | ${ this.renderLink('js', 'page', 'Page Reload') } 83 | ${ this.renderLink('js', 'off', 'Off') } 84 |
85 |
86 | 87 |
88 | css: ${ this.settings.strategies.css } 89 |
90 | ${ this.renderLink('css', 'page', 'Page Reload') } 91 | ${ this.renderLink('css', 'hot', 'Hot Reload') } 92 | ${ this.renderLink('css', 'off', 'Off') } 93 |
94 |
95 |
96 | html: ${ this.settings.strategies.html } 97 |
98 | ${ this.renderLink('html', 'page', 'Page Reload') } 99 | ${ this.renderLink('html', 'turbolinks', 'Turbolinks Reload', this.settings.turbolinksEnabled()) } 100 | ${ this.renderLink('html', 'wiselinks', 'Wiselinks Reload', this.settings.wiselinksEnabled()) } 101 | ${ this.renderLink('html', 'off', 'Off') } 102 |
103 |
104 | 105 |
106 | ruby: ${ this.settings.strategies.rb } 107 |
108 | ${ this.renderLink('rb', 'page', 'Page Reload') } 109 | ${ this.renderLink('rb', 'turbolinks', 'Turbolinks Reload', this.settings.turbolinksEnabled()) } 110 | ${ this.renderLink('rb', 'wiselinks', 'Wiselinks Reload', this.settings.wiselinksEnabled()) } 111 | ${ this.renderLink('rb', 'off', 'Off') } 112 |
113 |
114 | `); 115 | } 116 | 117 | renderLink(type, strategy, text, enabled = true) { 118 | const active = this.settings.strategies[type] === strategy; 119 | 120 | if (!enabled) { 121 | return ''; 122 | } 123 | 124 | return (` 125 | 130 |
${ text }
131 |
132 | `); 133 | } 134 | 135 | stylesheet() { 136 | return (` 137 | 235 | `); 236 | } 237 | } 238 | 239 | module.exports = StatusBar; 240 | -------------------------------------------------------------------------------- /spec/acceptance/install_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "tmpdir" 3 | 4 | RSpec.describe "Installing the Gem" do 5 | describe "Using generator" do 6 | specify do 7 | rails_dir = Dir.mktmpdir 8 | 9 | %x{rails new #{rails_dir}} 10 | 11 | open("#{rails_dir}/Gemfile", "a") do |file| 12 | file.write("gem 'breakfast', path: '#{Dir.pwd}'") 13 | end 14 | 15 | %x{cd #{rails_dir} && bundle install} 16 | %x{cd #{rails_dir} && bundle exec rails generate breakfast:install} 17 | %x{cd #{rails_dir} && node_modules/brunch/bin/brunch build} 18 | 19 | expect(File).to exist("#{rails_dir}/brunch-config.js") 20 | expect(File).to exist("#{rails_dir}/package.json") 21 | expect(File).to exist("#{rails_dir}/app/frontend/js/app.js") 22 | expect(File).to exist("#{rails_dir}/app/frontend/css/app.scss") 23 | expect(File.directory?("#{rails_dir}/node_modules")).to be true 24 | expect(File).to exist("#{rails_dir}/public/assets/app.css") 25 | expect(File).to exist("#{rails_dir}/public/assets/app.js") 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/breakfast/compilation_listener_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "tmpdir" 3 | 4 | RSpec.describe Breakfast::CompilationListener do 5 | let!(:asset_dir) { Dir.mktmpdir } 6 | let!(:source_code_dir) { Dir.mktmpdir } 7 | 8 | before do 9 | allow(Breakfast::CompilationListener).to receive(:broadcast) 10 | end 11 | 12 | describe ".start" do 13 | it "will listen for changes to the asset output folder" do 14 | Breakfast::CompilationListener.start( 15 | asset_output_folder: asset_dir, 16 | source_code_folders: source_code_dir 17 | ) 18 | 19 | ["css", "js"].each do |extension| 20 | open("#{asset_dir}/foo.#{extension}", "w") 21 | 22 | sleep(2) 23 | 24 | expect(Breakfast::CompilationListener). 25 | to have_received(:broadcast).with( 26 | Breakfast::RELOAD_CHANNEL, 27 | { extension: extension } 28 | ) 29 | end 30 | end 31 | 32 | it "will listen for changes to the source code folders" do 33 | Breakfast::CompilationListener.start( 34 | asset_output_folder: asset_dir, 35 | source_code_folders: source_code_dir 36 | ) 37 | 38 | ["rb", "html", "haml", "slim"].each do |extension| 39 | open("#{source_code_dir}/foo.#{extension}", "w") 40 | 41 | sleep(2) 42 | 43 | expect(Breakfast::CompilationListener). 44 | to have_received(:broadcast).with( 45 | Breakfast::RELOAD_CHANNEL, 46 | { extension: extension } 47 | ) 48 | 49 | expect(Breakfast::CompilationListener). 50 | to have_received(:broadcast).with(Breakfast::STATUS_CHANNEL, { 51 | status: "success", 52 | message: "saved: foo.#{extension}", 53 | extension: extension 54 | }) 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/breakfast/manifest_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "tmpdir" 3 | require "json" 4 | 5 | RSpec.describe Breakfast::Manifest do 6 | before do 7 | allow_any_instance_of(Digest::MD5).to receive(:hexdigest).and_return("digest") 8 | allow(SecureRandom).to receive(:hex) { "digest" } 9 | end 10 | 11 | let(:output_dir) { Dir.mktmpdir } 12 | 13 | it "will generate a manifest file and comiple digested assets" do 14 | Dir.mkdir("#{output_dir}/images/") 15 | 16 | app_js = File.open("#{output_dir}/app.js", "w") 17 | image = File.open("#{output_dir}/images/test.jpeg", "w") 18 | 19 | manifest = Breakfast::Manifest.new(base_dir: output_dir) 20 | manifest.digest! 21 | 22 | expect(File).to exist("#{output_dir}/app.js") 23 | expect(File).to exist("#{output_dir}/app-digest.js") 24 | 25 | expect(File).to exist("#{output_dir}/images/test.jpeg") 26 | expect(File).to exist("#{output_dir}/images/test-digest.jpeg") 27 | 28 | expect(JSON.parse(File.read("#{output_dir}/.breakfast-manifest-digest.json"))).to eq({ 29 | "app.js" => "app-digest.js", 30 | "images/test.jpeg" => "images/test-digest.jpeg" 31 | }) 32 | end 33 | 34 | it "will not fingerprint already fingerprinted assets" do 35 | File.open("#{output_dir}/app-523a40ea7f96cd5740980e61d62dbc77.js", "w") 36 | 37 | manifest = Breakfast::Manifest.new(base_dir: output_dir) 38 | manifest.digest! 39 | 40 | expect(File).to exist("#{output_dir}/app-523a40ea7f96cd5740980e61d62dbc77.js") 41 | expect(number_of_files(output_dir)).to eq(1) 42 | end 43 | 44 | it "will fingerprint assets that match the fingerprint regex somewhere else in their filename" do 45 | matching_regex_folder_name = "folder-aaaa11112222333444455556666bbbb8" 46 | Dir.mkdir("#{output_dir}/#{matching_regex_folder_name}/") 47 | File.open("#{output_dir}/#{matching_regex_folder_name}/app.js", "w") 48 | 49 | manifest = Breakfast::Manifest.new(base_dir: output_dir) 50 | manifest.digest! 51 | expect(number_of_files(output_dir)).to eq(2) 52 | end 53 | 54 | it "will find an existing manifest" do 55 | File.open("#{output_dir}/.breakfast-manifest-869269cdf1773ff0dec91bafb37310ea.json", "w") do |file| 56 | file.write({ "app.js" => "app-abc123.js" }.to_json) 57 | end 58 | 59 | manifest = Breakfast::Manifest.new(base_dir: output_dir) 60 | 61 | expect(manifest.asset("app.js")).to eq("app-abc123.js") 62 | end 63 | 64 | it "will return the digested asset path for a given asset" do 65 | Dir.mkdir("#{output_dir}/images/") 66 | 67 | app_js = File.open("#{output_dir}/app.js", "w") 68 | image = File.open("#{output_dir}/images/test.jpeg", "w") 69 | 70 | manifest = Breakfast::Manifest.new(base_dir: output_dir) 71 | manifest.digest! 72 | 73 | expect(manifest.asset("app.js")).to eq("app-digest.js") 74 | expect(manifest.asset("images/test.jpeg")).to eq("images/test-digest.jpeg") 75 | expect(manifest.asset("doesnt-exist.png")).to be nil 76 | end 77 | 78 | it "will remove assets that are no longer referenced by the manifest" do 79 | Dir.mkdir("#{output_dir}/images/") 80 | 81 | File.open("#{output_dir}/outdated-523a40ea7f96cd5740980e61d62dbc77.js", "w") 82 | File.open("#{output_dir}/app.js", "w") 83 | File.open("#{output_dir}/images/test.jpeg", "w") 84 | File.open("#{output_dir}/images/outdated-523a40ea7f96cd5740980e61d62dbc77.jpeg", "w") 85 | 86 | manifest = Breakfast::Manifest.new(base_dir: output_dir) 87 | manifest.digest! 88 | 89 | expect { manifest.clean! }.to change { number_of_files(output_dir) }.by(-2) 90 | end 91 | 92 | it "will keep any assets that are referenced by a sprockets manifest" do 93 | Dir.mkdir("#{output_dir}/images/") 94 | 95 | File.open("#{output_dir}/outdated-523a40ea7f96cd5740980e61d62dbc77.js", "w") 96 | File.open("#{output_dir}/app.js", "w") 97 | File.open("#{output_dir}/images/test.jpeg", "w") 98 | File.open("#{output_dir}/images/outdated-523a40ea7f96cd5740980e61d62dbc77.jpeg", "w") 99 | 100 | File.open("#{output_dir}/sprockets-file-4e936bdd95c293bccbeefc56f191e4a7.js", "w") 101 | File.open("#{output_dir}/.sprockets-manifest-4e936bdd95c293bccbeefc56f191e4a7.json", "w") do |file| 102 | file.write({ 103 | "files" => { 104 | "sprockets-file-4e936bdd95c293bccbeefc56f191e4a7.js" => { 105 | "logical_path"=>"sprockets-file.js", 106 | "mtime"=>"2016-10-26T18:26:19+00:00", 107 | "size"=>97551, 108 | "digest"=>"4e936bdd95c293bccbeefc56f191e4a7", 109 | "integrity"=>"sha256-hTHBWGfDx5DSg9+fD8EiCDkSZOCUpE+CNFjiFhKmICZ=" 110 | } 111 | }, 112 | "assets" => { 113 | "sprockets-file.js" => "sprockets-file-4e936bdd95c293bccbeefc56f191e4a7" 114 | } 115 | }.to_json) 116 | end 117 | 118 | manifest = Breakfast::Manifest.new(base_dir: output_dir) 119 | manifest.digest! 120 | 121 | expect { manifest.clean! }.to change { number_of_files(output_dir) }.by(-2) 122 | end 123 | 124 | def number_of_files(dir) 125 | Dir["#{dir}/**/*"].reject { |f| File.directory?(f) }.size 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /spec/breakfast_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Breakfast do 4 | it 'has a version number' do 5 | expect(Breakfast::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 2 | require "rails/all" 3 | require "breakfast" 4 | 5 | RSpec.configure do |config| 6 | config.disable_monkey_patching! 7 | config.order = :random 8 | config.expose_dsl_globally = true 9 | 10 | Kernel.srand config.seed 11 | end 12 | --------------------------------------------------------------------------------