How to set up importmap-rails in Rails 7 engine? - ruby-on-rails

I have opened an issue in the importmap-rails gem github repository here about this but thought I'd throw the question out here in case anyone might have a workaround
This is what I have discovered so far
A new engine with Rails 7 alpha 2 or Rails 7.0, generated using rails plugin new custom_page --mountable --full generates a new engine that includes the importmap-rails gem in the bundled gems but there is no ability to use it. Adding spec.add_dependency 'importmap-rails' to the enginename.gemspec makes no difference, nor does adding a require importmap-rails to engine.rb. There is no importmap executable in the bin directory.
A call to bundle info importmap-rails
Produces a promising result showing that the gem is installed by default
* importmap-rails (0.8.1)
Summary: Use ESM with importmap to manage modern JavaScript in Rails without transpiling or bundling.
Homepage: https://github.com/rails/importmap-rails
Source Code: https://github.com/rails/importmap-rails
Path: /home/jamie/.rvm/gems/ruby-3.0.0#custom_page/gems/importmap-rails-0.8.1
A call to rails --tasks shows
rails app:importmap:install # Setup Importmap for the app
But I believe this is coming from the test application generated by the --full option rather than being available to the rails command for the engine.
I was expecting to see the same without app: prefix
A call to this task resolves to a template error as shown
rails app:importmap:install
Don't know how to build task 'app:template' (See the list of available
tasks with rails --tasks) Did you mean? app:tmp:create
If there is a workaround solution to this I'd be grateful to hear it and I'm sure others will too. The reason for me wanting this is that I totally failed to introduced webpacker in a rails 6.1.4 engine and I was hoping this was going to be my, much improved, solution

You don't need to use the install task to set up importmaps. All it does is a few copy paste operations and it doesn't really help with the engine set up anyway.
Add importmaps to engine's gemspec file:
# my_engine/my_engine.gemspec
spec.add_dependency "importmap-rails"
Update engine.rb:
# my_engine/lib/my_engine/engine.rb
require "importmap-rails"
module MyEngine
class Engine < ::Rails::Engine
isolate_namespace MyEngine
initializer "my-engine.importmap", before: "importmap" do |app|
# NOTE: this will add pins from this engine to the main app
# https://github.com/rails/importmap-rails#composing-import-maps
app.config.importmap.paths << root.join("config/importmap.rb")
# NOTE: something about cache; I did not look into it.
# https://github.com/rails/importmap-rails#sweeping-the-cache-in-development-and-test
app.config.importmap.cache_sweepers << root.join("app/assets/javascripts")
end
# NOTE: add engine manifest to precompile assets in production
initializer "my-engine.assets" do |app|
app.config.assets.precompile += %w[my_engine_manifest]
end
end
end
Update assets manifest:
# my_engine/app/assets/config/my_engine_manifest.js
//= link_tree ../javascripts/my_engine .js
Add javascript entry point for our engine, if needed. Pins will be available without this file.
# my_engine/app/assets/javascripts/my_engine/application.js
// do some javascript
document.querySelector("h1").innerText = "hi, i'm your engine";
console.log("hi, again");
Update engine's layout:
# my_engine/app/views/layouts/my_engine/application.html.erb
<!DOCTYPE html>
<html>
<head>
<!--
NOTE: This loads/imports main app `application.js` and all the pins from
the main app and from the engine (because we set it up in the engine.rb).
-->
<%= javascript_importmap_tags %>
<!--
NOTE: To add engine's javascript functionality we have to load the
entrypoint here or `import` it in the main app `application.js`
-->
<%= javascript_import_module_tag "my_engine/application" %>
</head>
<body> <%= yield %> </body>
</html>
Create importmap.rb and pin my_engine/application, this name has to match with javascript_import_module_tag. It cannot clash with any other name in the main app, so you can't just use application:
# my_engine/config/importmap.rb
# NOTE: this pin works because `my_engine/app/assets/javascripts
# is in the `Rails.application.config.assets.paths`
pin "my_engine/application"
Some extras to test the setup:
# config/routes.rb
Rails.application.routes.draw do
mount MyEngine::Engine => "/"
end
# my_engine/config/routes.rb
MyEngine::Engine.routes.draw do
get "home", to: "homes#index"
end
# my_engine/app/controllers/my_engine/homes_controller.rb
module MyEngine
class HomesController < ApplicationController
def index; end
end
end
# my_engine/app/views/my_engine/homes/index.html.erb
<h1>Home</h1>
At this point you should have this in your rendered layout's <head> tag, among other things:
<script type="importmap" data-turbo-track="reload">{
"imports": {
"application": "/assets/application-66ce7505c61e3e4910ff16e7c220e1fbfb39251cd82e4bab8d325b3aae987cf9.js",
"my_engine/application": "/assets/my_engine/application-31ce493e8376b4c20703a50f38d419ae309ffe410b7ab7fec47440e02eef08a8.js",
}
}</script>
<script type="module">import "application"</script>
<script type="module">import "my_engine/application"</script>
H1 tag should change to <h1>hi, i'm your engine</h1> on reload.
Additional importmaps can be added manually with https://generator.jspm.io/.
For bonus points, bin/importmap can be customized to work inside the engine. Create a new importmap file inside bin directory.
# my_engine/bin/importmap
#!/usr/bin/env ruby
# NOTE: don't forget to `chmod u+x bin/importmap` to make it executable.
# make sure we are loading the correct versions of things
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
# NOTE: importmap requires some rails goodness that we don't have in the engine,
# because we don't have `config/application.rb` that loads the environment.
require "rails"
# importmap-rails is not loaded automatically
require "importmap-rails"
# the actual command runner
require "importmap/commands"
Run from inside the engine directory:
$ bin/importmap pin react
Pinning "react" to https://ga.jspm.io/npm:react#18.1.0/index.js
$ cat config/importmap.rb
pin "my_engine/application"
pin "react", to: "https://ga.jspm.io/npm:react#18.1.0/index.js"
I haven't tested it too much, so any feedback would be welcome. Restart the server if something doesn't show up, I don't know how reloading behaves with all this.

I fall back to the old school Javascript include in the html.
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.js"></script>
Well it definitely works on all browser instead of figuring out if the browser supports the feature that I might use later.
I have full control which page to put too... but that might not be what you want...

Related

Running webpacker:compile for a engine in my host app causes rails to abort

I have a rails engine/plugin in which i am trying to use webpacker using THIS article as a guide. So basically in my engine, i have the following code :-
lib/saddlebag.rb
require "saddlebag/engine"
module Saddlebag
# ...
class << self
def Webpacker
#Webpacker ||= ::Webpacker::Instance.new(
root_path: Saddlebag::Engine.root,
config_path: Saddlebag::Engine.root.join('config', 'webpacker.yml')
)
end
end
# ...
end
and in the lib/saddlebag/engine.rb file i have the following code :
module Saddlebag
class Engine < ::Rails::Engine
isolate_namespace Saddlebag
# use packs from saddlebag via Rack static
# file service, to enable webpacker to find them
# when running in the host application
config.app_middleware.use(
Rack::Static,
# note! this varies from the webpacker/engine documentation
urls: ["/saddlebag-packs"], root: Saddlebag::Engine.root.join("public")
)
initializer "webpacker.proxy" do |app|
insert_middleware = begin
Saddlebag.webpacker.config.dev_server.present?
rescue
nil
end
next unless insert_middleware
app.middleware.insert_before(
0, Webpacker::DevServerProxy, # "Webpacker::DevServerProxy" if Rails version < 5
ssl_verify_none: true,
webpacker: Saddlebag.webpacker
)
end
end
end
Also i have all of the files required by webpacker mainly :-
config/webpacker.yml and config/webpack/*.js files
bin/webpack and bin/webpack-dev-server files
package.json with required deps.
So the engine and my actually app are in sibling directories so:-
saddlebag
open-flights (main app)
in open flights i link saddlebag with the following line in the gem file :-
gem 'saddlebag', path: '../saddlebag'
Now when i run bin/rails saddlebag:webpacker:compile , i get the following error :-
rails aborted! Don't know how to build task
'saddlebag:webpacker:compile' (See the list of available tasks with
rails --tasks)
Why am i getting this error i have webpacker as a dependency in my saddlebag app. So not sure why this error still occures.
P.S. I found a similar guide for rails engine for enabling webpacker HERE (but uses docker)

How to use tailwind css gem in a rails 7 engine?

How to use tailwind in a rails engine? According to the documentation supplying a css argument to the Rails generator should work
Rails 7.0.2.2 engine generated using
rails plugin new tailtest --mountable --full -d postgresql --css tailwind
This generates the engine with Postgresql but does nothing with tailwind at all, and following manual installation instructions fail too.
Running, as per documentation, bundle add tailwindcss-rails adds tailwind to the gemfile rather than the engines tailtest.gemspec
So after adding the dependency to the gemspec
spec.add_dependency "tailwindcss-rails", "~> 2.0"
and running bundle install does install the engine however the rest of the manual installation fails
then adding the require to lib/engine.rb
require "tailwindcss-rails"
module Tailtest
class Engine < ::Rails::Engine
isolate_namespace Tailtest
end
end
then running the install process fails
rails tailwindcss:install
Resolving dependencies...
rails aborted!
Don't know how to build task 'tailwindcss:install' (See the list of available tasks with `rails --tasks`)
Did you mean? app:tailwindcss:install
Obviously the app:tailwindcss:install command fails too.
So I am probably missing an initializer of some sort in the engine.rb file but no idea on what it should be.
It is the same idea as How to set up importmap-rails in Rails 7 engine?. We don't need to use the install task. Even if you're able to run it, it's not helpful in the engine (see the end of the answer for explanation).
Also rails plugin new doesn't have a --css option. To see available options: rails plugin new -h.
Update engine's gemspec file:
# my_engine/my_engine.gemspec
spec.add_dependency "tailwindcss-rails"
Update engine.rb:
# my_engine/lib/my_engine/engine.rb
module MyEngine
class Engine < ::Rails::Engine
isolate_namespace MyEngine
# NOTE: add engine manifest to precompile assets in production, if you don't have this yet.
initializer "my-engine.assets" do |app|
app.config.assets.precompile += %w[my_engine_manifest]
end
end
end
Update assets manifest:
# my_engine/app/assets/config/my_engine_manifest.js
//= link_tree ../builds/ .css
Update engine's layout:
# my_engine/app/views/layouts/my_engine/application.html.erb
<!DOCTYPE html>
<html>
<head>
<%#
NOTE: make sure this name doesn't clash with anything in the main app.
think of it as `require` and `$LOAD_PATH`,
but instead it is `stylesheet_link_tag` and `manifest.js`.
%>
<%= stylesheet_link_tag "my_engine", "data-turbo-track": "reload" %>
</head>
<body> <%= yield %> </body>
</html>
bundle show command will give us the path where the gem is installed, so we can copy a few files:
$ bundle show tailwindcss-rails
/home/alex/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/tailwindcss-rails-2.0.8-x86_64-linux
Copy tailwind.config.js file from tailwindcss-rails:
$ cp $(bundle show tailwindcss-rails)/lib/install/tailwind.config.js config/tailwind.config.js
Copy application.tailwind.css file into any directory to fit your setup:
$ cp $(bundle show tailwindcss-rails)/lib/install/application.tailwind.css app/assets/stylesheets/application.tailwind.css
Because tailwindcss-rails uses standalone executable, we don't need node or rails to compile the stylesheets. We just need to get to the executable itself.
Executable is located here https://github.com/rails/tailwindcss-rails/tree/v2.0.8/exe/. Instead of running the build task https://github.com/rails/tailwindcss-rails/blob/v2.0.8/lib/tasks/build.rake we can just call the executable directly.
$ $(bundle show tailwindcss-rails)/exe/tailwindcss -i app/assets/stylesheets/application.tailwind.css -o app/assets/builds/my_engine.css -c config/tailwind.config.js --minify
Use -w option to start watch mode.
$ $(bundle show tailwindcss-rails)/exe/tailwindcss -i app/assets/stylesheets/application.tailwind.css -o app/assets/builds/my_engine.css -c config/tailwind.config.js --minify -w
The output file should match the name in stylesheet_link_tag "my_engine".
Now that you have a plain my_engine.css file, do with it what you want. Use it in the layout, require it from the main app application.css. The usual rails asset pipeline rules apply.
If you want to put all that into a task, use Engine.root to get the paths.
# my_engine/lib/tasks/my_engine.rake
task :tailwind_engine_watch do
require "tailwindcss-rails"
# NOTE: tailwindcss-rails is an engine
system "#{Tailwindcss::Engine.root.join("exe/tailwindcss")} \
-i #{MyEngine::Engine.root.join("app/assets/stylesheets/application.tailwind.css")} \
-o #{MyEngine::Engine.root.join("app/assets/builds/my_engine.css")} \
-c #{MyEngine::Engine.root.join("config/tailwind.config.js")} \
--minify -w"
end
From the engine directory:
$ bin/rails app:tailwind_engine_watch
+ /home/alex/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/tailwindcss-rails-2.0.8-x86_64-linux/exe/x86_64-linux/tailwindcss -i /home/alex/code/stackoverflow/my_engine/app/assets/stylesheets/application.tailwind.css -o /home/alex/code/stackoverflow/my_engine/app/assets/builds/my_engine.css -c /home/alex/code/stackoverflow/my_engine/config/tailwind.config.js --minify -w
Rebuilding...
Done in 549ms.
Make your own install task if you have a lot of engines to set up:
desc "Install tailwindcss into our engine"
task :tailwind_engine_install do
require "tailwindcss-rails"
# NOTE: use default app template, which will fail to modify layout, manifest,
# and the last command that compiles the initial `tailwind.css`.
# It will also add `bin/dev` and `Procfile.dev` which we don't need.
# Basically, it's useless in the engine as it is.
template = Tailwindcss::Engine.root.join("lib/install/tailwindcss.rb")
# TODO: better to copy the template from
# https://github.com/rails/tailwindcss-rails/blob/v2.0.8/lib/install/tailwindcss.rb
# and customize it
# template = MyEngine::Engine.root("lib/install/tailwindcss.rb")
require "rails/generators"
require "rails/generators/rails/app/app_generator"
# NOTE: because the app template uses `Rails.root` it will run the install
# on our engine's dummy app. Just override `Rails.root` with our engine
# root to run install in the engine directory.
Rails.configuration.root = MyEngine::Engine.root
generator = Rails::Generators::AppGenerator.new [Rails.root], {}, { destination_root: Rails.root }
generator.apply template
end
Install task reference:
https://github.com/rails/rails/blob/v7.0.2.4/railties/lib/rails/tasks/framework.rake#L8
https://github.com/rails/tailwindcss-rails/blob/v2.0.8/lib/tasks/install.rake
Watch task reference:
https://github.com/rails/tailwindcss-rails/blob/v2.0.8/lib/tasks/build.rake#L10
Update How to merge two tailwinds.
Above setup assumes the engine is its own separate thing, like admin backend, it has its own routes, templates, and styles. If an engine functionality is meant to be mixed with the main app, like a view_component collection, then tailwind styles will override each other. In this case isolating engine styles with a prefix could work:
https://tailwindcss.com/docs/configuration#prefix
The reason that tailwind styles don't mix is because most of the selectors have the same specificity and the order is very important.
So here is an example. Main app with an engine, both using tailwind, both compile styles separately, tailwind configs are only watching one file from the engine and one from the main app, only using #tailwind utilities; directive:
Engine template, that we want to use in the main app, should work fine:
<!-- blep/app/views/blep/_partial.html.erb -->
<div class="bg-red-500 sm:bg-blue-500"> red never-blue </div>
But when rendered in the main app it never turns blue. Here is the demonstration set up:
<!-- app/views/home/index.html.erb -->
<%= stylesheet_link_tag "blep", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
<!-- output generated css in the same order as above link tags -->
<% require "open-uri" %>
<b>Engine css</b>
<pre><%= URI.open(asset_url("blep")).read %></pre>
<b>Main app css</b>
<pre><%= URI.open(asset_url("tailwind")).read %></pre>
<div class="bg-red-500"> red </div> <!-- this generates another bg-red-500 -->
<br>
<%= render "blep/partial" %>
And it looks like this:
/* Engine css */
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity))
}
#media (min-width: 640px) {
.sm\:bg-blue-500 {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity))
}
}
/* Main app css */
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity))
}
<div class="bg-red-500"> red </div>
<br>
<div class="bg-red-500 sm:bg-blue-500"> red never-blue </div>
^ you can hit run and click "full page". Main app bg-red-500 selector is last so it overrides engines sm:bg-blue-500 selector, media queries don't add to specificity score. It's the same reason you can't override, say, mt-1 with m-2, margin top comes later in the stylesheet. This is why #layer directives are important.
The only way around this is to watch the engine directory when running tailwind in the main app, so that styles are compiled together and in the correct order. Which means you don't really need tailwind in the engine:
module.exports = {
content: [
"./app/**/*",
"/just/type/the/path/to/engine/views",
"/or/see/updated/task/below",
],
}
Other ways I tried, like running 6 tailwind commands for each layer for main app and engine, so that I can put them in order, better but was still out of order a bit and duplicated. Or doing an #import and somehow letting postcss-import know where to look for engine styles (I don't know, I just symlinked it into node_modules to test), but this still required tailwind to watch engine files.
I did some more digging, tailwind cli has a --content option, which will override content from tailwind.config.js. We can use it to setup a new task:
namespace :tailwindcss do
desc "Build your Tailwind CSS + Engine"
task :watch do |_, args|
# NOTE: there have been some updates, there is a whole Commands class now
# lets copy paste and modify. (debug = no --minify)
command = Tailwindcss::Commands.watch_command(debug: true, poll: false)
# --content /path/to/app/**/*,/path/to/engine/**/*
command << "--content"
command << [
Rails.root.join("app/views/home/*"),
Blep::Engine.root.join("app/views/**/*.erb")
].join(",")
p command
system(*command)
end
# same for build, just call `compile_command`
# task :build do |_, args|
# command = Tailwindcss::Commands.compile_command(debug: false)
# ...
end
https://github.com/rails/tailwindcss-rails/blob/v2.0.21/lib/tasks/build.rake#L11
That answer by Alex is really good, i wish i had it when starting out. (But i didn't even have the question to google)
Just want to add two things:
1- a small simplification. I just made a script to run tailwind in the engine
#!/usr/bin/env sh
# Since tailwind does not install into the engine, this will
# watch and recompile during development
# tailwindcss executable must exist (by bundling tailwindcss-rails eg)
tailwindcss -i app/assets/stylesheets/my_engine.tailwind.css \
-o app/assets/stylesheets/my_engine/my_engine.css \
-c config/tailwind.config.js \
-w
2- For usage in an app, that obviously also uses tailwind, i was struggling, since the two generated css's were biting each other and i could not get both styles to work in one page. Always one or the other (app or engine) was not styled right. Until i got the app's tailwind to pick up the engines classes.
Like so:
Add to the app's tailwind.config.js: before the module
const execSync = require('child_process').execSync;
const output = execSync('bundle show my_engine', { encoding: 'utf-8' });
And then inside the content as last line
output.trim() + '/app/**/*.{erb,haml,html,rb}'
Then just include the apps generated tailwind css in the layout, like the installer will. Don't include the engines stylesheet in the layout, or add it to the asset

Rspec uninitialized constant for class nested inside module - Ruby on Rails

In a Rails project, I have created a new class inside the lib directory, this class is namespaced inside a module. When creating a spec file for it, I'm seeing NameError: uninitialized constant MyNamespace.
Here is my folder structure
app/
...
lib
my_namespace
my_new_class.rb
another_namespace
another_old_class.rb
spec
lib
my_namespace
my_new_class_spec.rb
another_namespace
another_old_class_spec.rb
Here the (abbreviated) contents of:
lib/my_namespace/my_new_class.rb
module MyNamespace
class MyNewClass
end
end
spec/lib/my_namespace/my_new_class_spec.rb
RSpec.describe MyNamespace::MyNewClass do
it "is true"
expect(true).to eq(true) # irrelevant at this point
end
end
The reason I included another_old_class_spec.rb is that its tests run without issue and I can't find anywhere that it's explicitly loaded or required in the test setup (in case that might be a potential issue).
When running the test with bundle exec rspec spec/lib/my_namespace/my_new_class_spec.rb or even bundle exec rspec spec/lib/my_namespace/ I get
An error occurred while loading ./spec/lib/my_namespace/my_new_class_spec.rb
Failure/Error:
RSpec.describe MyNamespace::MyNewClass do
NameError:
uninitialized constant MyNamespace
Like Georgiy Melnikov implied in his comment, by default the /lib directory is not in autoload paths, so the constant MyNamespace is not automatically resolved.
You basically have two options to fix this:
explicitly require the file with require lib/my_namespace/my_new_class at the top of the spec file
add lib/ to autoload paths (nowadays this is discouraged)

Expiring the Sprockets cache from Rails (not Rake)

I have a special scenario where if a specific file changes, I need to expire the Sprockets cache (all stylesheets) from Ruby/Rails.
My (failed) approach so far is to prepend dependency_fresh? and attempt to expire the index.
module SprocketsAssetWrapper
def dependency_fresh?(environment, dep)
if <file is changed>
# Attempt to expire the Sprocket's Environment
# Errors with "can't modify immutable index"
environment.send(:expire_index!)
<reset file as unchanged>
end
super environment, dep
end
end
class Sprockets::Asset
prepend SprocketsAssetWrapper
end
As commented, environment.send(:expire_index!) errors with can't modify immutable index.
Any help would be great. I'm on Rails 4.1.4 and have sprockets-rails installed.
Thanks,
Erik

Change a RDoc template for generating Rails app documentation

I have just added some documentation to my Rails 3.2.13 app. I can generate the documentation just fine (running RDoc 3.12.2) by using a rake task:
# lib/tasks/documentation.rake
Rake::Task["doc:app"].clear
Rake::Task["doc/app"].clear
Rake::Task["doc/app/index.html"].clear
namespace :doc do
RDoc::Task.new('app') do |rdoc|
rdoc.rdoc_dir = 'doc/app'
rdoc.generator = 'hanna'
rdoc.title = 'Stoffi Web App Documentation'
rdoc.main = 'doc/Overview'
rdoc.options << '--charset' << 'utf-8'
rdoc.rdoc_files.include('app/**/*.rb')
rdoc.rdoc_files.include('doc/*')
end
end
...and then running rake doc:app. But I really don't like the default look of the Hanna template. Is there a way to edit the CSS, perhaps by providing my own CSS file which will override the default one used in Hanna?
Thanks!
First of all find where your templates are located:
⮀ RDPATH=$(dirname $(gem which rdoc))
# ⇒ /home/am/.rvm/rubies/ruby-head/lib/ruby/2.1.0
Now copy the default template from there to the desired location (change /tmp to your project directory or like):
⮀ cp -r $RDPATH/rdoc/generator/template/darkfish /tmp/myniftytemplate
And, finally, let’s teach the rdoc:
class RDoc::Options
def template_dir_for template
"/tmp/#{template}"
end
end
RDoc::Task.new('app') do |rdoc|
rdoc.template = 'myniftytemplate'
…
end
That’s it. Hope it helps.

Resources