├── .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 |
85 |
86 |
87 |
88 | css: ${ this.settings.strategies.css }
89 |
94 |
95 |
96 | html: ${ this.settings.strategies.html }
97 |
103 |
104 |
105 |
106 | ruby: ${ this.settings.strategies.rb }
107 |
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 |
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 |
--------------------------------------------------------------------------------