Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

importmap-rails in Rails 7, is this possible?

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

like image 831
jamesc Avatar asked Nov 07 '22 00:11

jamesc


1 Answers

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:[email protected]/index.js

$ cat config/importmap.rb 
pin "my_engine/application"
pin "react", to: "https://ga.jspm.io/npm:[email protected]/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.

like image 87
Alex Avatar answered Nov 12 '22 17:11

Alex