Skip to content

Add support for installing via rails mounted engines #162

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
*.gem
/node_modules

test/rake_tasks/mounted_apps/webpacker/node_modules
test/rake_tasks/mounted_apps/webpacker/log
test/rake_tasks/mounted_apps/asset_pipeline/log
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ group :test do
gem 'selenium-webdriver'
gem 'webdrivers'
gem 'sqlite3'
gem 'webpacker'
end
9 changes: 9 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ GEM
nio4r (~> 2.0)
racc (1.5.2)
rack (2.2.3)
rack-proxy (0.6.5)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.1.3.2)
Expand Down Expand Up @@ -140,6 +142,7 @@ GEM
selenium-webdriver (3.142.7)
childprocess (>= 0.5, < 4.0)
rubyzip (>= 1.2.2)
semantic_range (3.0.0)
sprockets (4.0.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
Expand All @@ -155,6 +158,11 @@ GEM
nokogiri (~> 1.6)
rubyzip (>= 1.3.0)
selenium-webdriver (>= 3.0, < 4.0)
webpacker (5.2.1)
activesupport (>= 5.2)
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
Expand All @@ -175,6 +183,7 @@ DEPENDENCIES
sqlite3
turbo-rails!
webdrivers
webpacker
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this dev dependency to test the app:turbo:webpacker:install task


BUNDLED WITH
2.2.16
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ The `Turbo` instance is automatically assigned to `window.Turbo` upon import:
```js
import "@hotwired/turbo-rails"
```
### Installing on Mounted Engines
The setup for use in a mounted engine will involve the following steps:

1. Add the `turbo-rails` gem to your gemspec file: `spec.add_dependency "turbo-rails"`
2. Run `./bin/bundle install`
3. Run `./bin/rails app:turbo:install`
4. Add `require "turbo-rails"` to your Rails::Engine subclass file (e.g. `lib/blorgh/engine.rb`)
5. Add a similar initializer for your assets to that same Rails::Engine file:
```ruby
initializer "blorgh.assets.precompile" do |app|
app.config.assets.precompile += %w( blorgh/application.css )
end
```

## Usage

Expand Down
63 changes: 47 additions & 16 deletions lib/install/turbo_with_asset_pipeline.rb
Original file line number Diff line number Diff line change
@@ -1,36 +1,67 @@
APPLICATION_LAYOUT_PATH = Rails.root.join("app/views/layouts/application.html.erb")
IMPORTMAP_PATH = Rails.root.join("app/assets/javascripts/importmap.json.erb")
CABLE_CONFIG_PATH = Rails.root.join("config/cable.yml")
app_layout_path = Rails.root.join("app/views/layouts/application.html.erb")
importmap_path = Rails.root.join("app/assets/javascripts/importmap.json.erb")
cable_config_path = Rails.root.join("config/cable.yml")

if APPLICATION_LAYOUT_PATH.exist?
def engine_root
defined?(ENGINE_ROOT) && Pathname.new(ENGINE_ROOT)
end

if engine_root
engine_name = File.basename(ENGINE_ROOT)
app_layout_path = engine_root.join("app/views/layouts/#{engine_name}/application.html.erb")
importmap_path = engine_root.join("app/assets/javascripts/#{engine_name}/importmap.json.erb")
end

if app_layout_path.exist?
say "Yield head in application layout for cache helper"
insert_into_file APPLICATION_LAYOUT_PATH.to_s, "\n <%= yield :head %>", before: /\s*<\/head>/
insert_into_file app_layout_path.to_s, "\n <%= yield :head %>", before: /\s*<\/head>/

if APPLICATION_LAYOUT_PATH.read =~ /stimulus/
if app_layout_path.read =~ /stimulus/
say "Add Turbo include tags in application layout"
insert_into_file APPLICATION_LAYOUT_PATH.to_s, %(\n <%= javascript_include_tag "turbo", type: "module-shim" %>), after: /<%= stimulus_include_tags %>/
insert_into_file app_layout_path.to_s, %(\n <%= javascript_include_tag "turbo", type: "module-shim" %>), after: /<%= stimulus_include_tags %>/

if IMPORTMAP_PATH.exist?
if importmap_path.exist?
say "Add Turbo to importmap"
insert_into_file IMPORTMAP_PATH, %( "turbo": "<%= asset_path "turbo" %>",\n), after: / "imports": {\s*\n/
insert_into_file importmap_path, %( "turbo": "<%= asset_path "turbo" %>",\n), after: / "imports": {\s*\n/
end
else
say "Add Turbo include tags in application layout"
insert_into_file APPLICATION_LAYOUT_PATH.to_s, %(\n <%= javascript_include_tag "turbo", type: "module" %>), before: /\s*<\/head>/
insert_into_file app_layout_path.to_s, %(\n <%= javascript_include_tag "turbo", type: "module" %>), before: /\s*<\/head>/
end
else
say "Default application.html.erb is missing!", :red
say %( Add <%= javascript_include_tag("turbo", type: "module-shim") %> and <%= yield :head %> within the <head> tag after Stimulus includes in your custom layout.)
end

if CABLE_CONFIG_PATH.exist?
say "Enable redis in bundle"
uncomment_lines "Gemfile", %(gem 'redis')
if engine_root
say "Appending redis to Gemfile"
append_to_file engine_root.join("Gemfile"), %(gem 'redis')

say "Switch development cable to use redis"
gsub_file CABLE_CONFIG_PATH.to_s, /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
say "Create config/cable.yml to use redis"
create_file engine_root.join("config/cable.yml") do
<<~FILE
development:
adapter: redis
url: redis://localhost:6379/1
test:
adapter: test

production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: dummy_production
FILE
end
else
say 'ActionCable config file (config/cable.yml) is missing. Uncomment "gem \'redis\'" in your Gemfile and create config/cable.yml to use the Turbo Streams broadcast feature.'
if cable_config_path.exist?
say "Enable redis in bundle"
uncomment_lines "Gemfile", %(gem 'redis')

say "Switch development cable to use redis"
gsub_file cable_config_path, /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
else
say 'ActionCable config file (config/cable.yml) is missing. Uncomment "gem \'redis\'" in your Gemfile and create config/cable.yml to use the Turbo Streams broadcast feature.'
end
end

say "Turbo successfully installed ⚡️", :green
37 changes: 30 additions & 7 deletions lib/install/turbo_with_webpacker.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Some Rails versions use commonJS(require) others use ESM(import).
TURBOLINKS_REGEX = /(import .* from "turbolinks".*\n|require\("turbolinks"\).*\n)/.freeze
ACTIVE_STORAGE_REGEX = /(import.*ActiveStorage|require.*@rails\/activestorage.*)/.freeze
CABLE_CONFIG_PATH = Rails.root.join("config/cable.yml")
cable_config_path = Rails.root.join("config/cable.yml")

abort "❌ Webpacker not found. Exiting." unless defined?(Webpacker::Engine)

Expand All @@ -16,14 +16,37 @@
gsub_file "#{Webpacker.config.source_entry_path}/application.js", TURBOLINKS_REGEX, ''
gsub_file "#{Webpacker.config.source_entry_path}/application.js", /Turbolinks.start.*\n/, ''

if CABLE_CONFIG_PATH.exist?
say "Enable redis in bundle"
uncomment_lines "Gemfile", %(gem 'redis')
engine_root = defined?(ENGINE_ROOT) && Pathname.new(ENGINE_ROOT)
if engine_root
say "Appending redis to Gemfile"
append_to_file engine_root.join("Gemfile"), %(gem 'redis')

say "Switch development cable to use redis"
gsub_file CABLE_CONFIG_PATH.to_s, /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
say "Create config/cable.yml to use redis"
create_file engine_root.join("config/cable.yml") do
<<~FILE
development:
adapter: redis
url: redis://localhost:6379/1

test:
adapter: test

production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: dummy_production
FILE
end
else
say 'ActionCable config file (config/cable.yml) is missing. Uncomment "gem \'redis\'" in your Gemfile and create config/cable.yml to use the Turbo Streams broadcast feature.'
if cable_config_path.exist?
say "Enable redis in bundle"
uncomment_lines "Gemfile", %(gem 'redis')

say "Switch development cable to use redis"
gsub_file cable_config_path, /development:\n\s+adapter: async/, "development:\n adapter: redis\n url: redis://localhost:6379/1"
else
say 'ActionCable config file (config/cable.yml) is missing. Uncomment "gem \'redis\'" in your Gemfile and create config/cable.yml to use the Turbo Streams broadcast feature.'
end
end

say "Turbo successfully installed ⚡️", :green
21 changes: 13 additions & 8 deletions lib/tasks/turbo_tasks.rake
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
def run_turbo_install_template(path) system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../install/#{path}.rb", __dir__)}" end
def run_turbo_install_template(path, nspace = nil)
system "#{RbConfig.ruby} #{Rails.root}/bin/rails #{nspace}app:template LOCATION=#{File.expand_path("../install/#{path}.rb", __dir__)}"
end

namespace :turbo do
desc "Install Turbo into the app"
task :install do
task :install do |task|
nspace = task.name.split(/turbo:install/).first
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ultimately the approach that I took to determine whether the rake task invocations should include the app namespace. An alternative approach I was considering is something like:

nspace = nil
nspace = "app:" if defined?(ENGINE_ROOT) 

I came across ENGINE_ROOT from one of the rails source files, but I don't know enough of rails to be confident that will always work.

if defined?(Webpacker::Engine)
Rake::Task["turbo:install:webpacker"].invoke
Rake::Task["#{nspace}turbo:install:webpacker"].invoke
else
Rake::Task["turbo:install:asset_pipeline"].invoke
Rake::Task["#{nspace}turbo:install:asset_pipeline"].invoke
end
end

namespace :install do
desc "Install Turbo into the app with asset pipeline"
task :asset_pipeline do
run_turbo_install_template "turbo_with_asset_pipeline"
task :asset_pipeline do |task|
nspace = task.name.split(/turbo:install/).first
run_turbo_install_template "turbo_with_asset_pipeline", nspace
end

desc "Install Turbo into the app with webpacker"
task :webpacker do
run_turbo_install_template "turbo_with_webpacker"
task :webpacker do |task|
nspace = task.name.split(/turbo:install/).first
run_turbo_install_template "turbo_with_webpacker", nspace
end
end
end
4 changes: 4 additions & 0 deletions test/fixtures/files/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
source 'https://rubygems.org'

# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
24 changes: 24 additions & 0 deletions test/fixtures/files/application.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title><%= camelized %></title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>

<%- if options[:skip_javascript] -%>
<%= stylesheet_link_tag "application" %>
<%- else -%>
<%- unless options[:skip_turbolinks] -%>
<%= stylesheet_link_tag "application", "data-turbolinks-track": "reload" %>
<%= javascript_pack_tag "application", "data-turbolinks-track": "reload" %>
<%- else -%>
<%= stylesheet_link_tag "application" %>
<%= javascript_pack_tag "application" %>
<%- end -%>
<%- end -%>
</head>

<body>
<%= yield %>
</body>
</html>
10 changes: 10 additions & 0 deletions test/fixtures/files/cable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
development:
adapter: async

test:
adapter: test

production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: blah_production
22 changes: 22 additions & 0 deletions test/rake_task_test_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module RakeTaskTestHelper
def setup
app_layout = File.join(fixtures_path, "application.html.erb")
gemfile = File.join(fixtures_path, "Gemfile")
cablefile = File.join(fixtures_path, "cable.yml")

FileUtils.mkpath File.join(app_path, "app", "views", "layouts")
FileUtils.cp app_layout, File.join(app_path, "app", "views", "layouts", "application.html.erb")
FileUtils.cp gemfile, File.join(app_path, "Gemfile")
FileUtils.cp cablefile, File.join(app_path, "config", "cable.yml")
end

def teardown
FileUtils.rm File.join(app_path, "app", "views", "layouts", "application.html.erb")
FileUtils.rm File.join(app_path, "Gemfile")
FileUtils.rm File.join(app_path, "config", "cable.yml")
end

def fixtures_path
File.expand_path("fixtures/files", __dir__)
end
end
25 changes: 25 additions & 0 deletions test/rake_tasks/engine_asset_pipeline_rake_tasks_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
require 'rake_task_test_helper'

class EngineAssetPipelineRakeTasksTest < Minitest::Test
include RakeTaskTestHelper

def test_tasks_are_listed_on_asset_pipeline_app
output = Dir.chdir(app_path) { `rake -T` }

assert_includes output, "app:turbo"
assert_includes output, "app:turbo:install"
assert_includes output, "app:turbo:install:asset_pipeline"
end

def test_asset_pipeline_based_install
output = Dir.chdir(app_path) { `bundle exec rake app:turbo:install` }

assert_includes output, "Turbo successfully installed ⚡"
end

private

def app_path
File.expand_path("mounted_apps/asset_pipeline", __dir__)
end
end
31 changes: 31 additions & 0 deletions test/rake_tasks/engine_webpacker_rake_tasks_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require 'rake_task_test_helper'

class EngineWebpackerRakeTasksTest < Minitest::Test
include RakeTaskTestHelper

def teardown
nmodules_path = File.join(app_path, "node_modules")
FileUtils.rm_rf nmodules_path if File.directory?(nmodules_path)
super
end

def test_tasks_are_listed_on_webpacker_app
output = Dir.chdir(app_path) { `rake -T` }

assert_includes output, "app:turbo"
assert_includes output, "app:turbo:install"
assert_includes output, "app:turbo:install:webpacker"
end

def test_webpacker_based_install
output = Dir.chdir(app_path) { `bundle exec rake app:turbo:install` }

assert_includes output, "Turbo successfully installed ⚡"
end

private

def app_path
File.expand_path("mounted_apps/webpacker", __dir__)
end
end
4 changes: 4 additions & 0 deletions test/rake_tasks/mounted_apps/asset_pipeline/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
require "bundler/setup"

APP_RAKEFILE = File.expand_path("app_rakefile", __dir__)
load "rails/tasks/engine.rake"
3 changes: 3 additions & 0 deletions test/rake_tasks/mounted_apps/asset_pipeline/app_rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require_relative "config/application"

Rails.application.load_tasks
3 changes: 3 additions & 0 deletions test/rake_tasks/mounted_apps/asset_pipeline/bin/rails
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env ruby
APP_PATH = File.expand_path("../config/application", __dir__)
require "rails/commands"
3 changes: 3 additions & 0 deletions test/rake_tasks/mounted_apps/asset_pipeline/bin/rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require "rake"
Rake.application.run
5 changes: 5 additions & 0 deletions test/rake_tasks/mounted_apps/asset_pipeline/config.ru
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# This file allows the `Rails.root` to be correctly determined.

require_relative "config/environment"

run Rails.application
Loading