11

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

Update in response to Alex

Running bin/importmap json within the engine directory results in

importmap-rails-1.2.1/lib/importmap/commands.rb:63:in json': undefined method join' for nil:NilClass (NoMethodError)

require Rails.root.join("config/environment")
                  ^^^^^
from /home/jamie/.rvm/gems/ruby-3.2.2@custom_page/gems/thor-1.2.2/lib/thor/command.rb:27:in `run'
from /home/jamie/.rvm/gems/ruby-3.2.2@custom_page/gems/thor-1.2.2/lib/thor/invocation.rb:127:in `invoke_command'
from /home/jamie/.rvm/gems/ruby-3.2.2@custom_page/gems/thor-1.2.2/lib/thor.rb:392:in `dispatch'
from /home/jamie/.rvm/gems/ruby-3.2.2@custom_page/gems/thor-1.2.2/lib/thor/base.rb:485:in `start'
from /home/jamie/.rvm/gems/ruby-3.2.2@custom_page/gems/importmap-rails-1.2.1/lib/importmap/commands.rb:147:in `<top (required)>'
from <internal:/home/jamie/.rvm/rubies/ruby-3.2.2/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:37:in `require'
from <internal:/home/jamie/.rvm/rubies/ruby-3.2.2/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:37:in `require'
from bin/importmap:24:in `<main>'

This engine functionality is not in an app yet

There are no importmap error messages in my logs, searched for "Importmap skipped missing path" and "skipped" and "importmap", the last did throw up errors from an earlier bug but nothing since that bug was resolved

I have a standard assets setup, I do have a feature that updates css files which may be what you are thinking of.

I have no compiled assets and all files are named import and import.rb in the singular

The following code in my application.js is as follows

// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails

my manifest

//= link_directory ../stylesheets/ccs_cms/custom_page .css
//= link_tree ../javascripts/ccs_cms/custom_page .js
//= link_tree ../javascripts/new_ckeditor
//= link ccs_cms/custom_page/jqtree.css
//= link ccs_cms/custom_page/public.css

my application.js looks like

// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails

//= require ccs_cms/custom_page/jquery-3.6.0.min
//= require ccs_cms/custom_page/jquery-ui.min
//= require ccs_cms/custom_page/tree.jquery
//= require ccs_cms/custom_page/sortable
//= require ccs_cms/custom_page/nested_fields/addFields
//= require_tree .

//require("./nested-forms/addFields");
//require("./nested-forms/removeFields");

// do some javascript
document.querySelector("h1").innerText = "Hi!, i'm your engine";
console.log("hi, again");

importmap.rb contains the following

# my_engine/config/importmap.rb

# NOTE: this pin works because `my_engine/app/assets/javascripts
#       is in the `Rails.application.config.assets.paths`
pin "ccs_cms/custom_page/application"

The following is generated in the head

<script type="importmap" data-turbo-track="reload">{
  "imports": {
  }
}</script>

along with

<script type="module">import "application"</script>

<script type="module">import "ccs_cms/custom_page/application"</script>

A further update: I do see the hi again message in the console so the console.log("hi, again"); works but the document.querySelector("h1").innerText = "Hi!, i'm your engine"; has no effect

Finally, adding

  initializer :append_importmap_paths do |app|
    app.config.importmap.paths << root.join("config/importmap.rb")
  end

to the engine.rb resolved the last piece of the jigsaw

1
  • 1
    Actually, you don't have app:template, but you should have app:app:template. My workaround is to create an alias rake task to bypass this error. in your rake file : desc 'Alias to app:app:template' task template: :environment do Rake::Task['app:app:template'].invoke end
    – gillien
    Commented Mar 15, 2022 at 9:25

2 Answers 2

18

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.

9
  • 1
    Thank you so much for yet another detailed answer. I'll check this out soon and if it works for me I'll let you know
    – jamesc
    Commented May 4, 2022 at 10:39
  • 1
    Instead of using both <%= javascript_importmap_tags %> and <%= javascript_import_module_tag "my_engine/application" %>, you can use a single <%= javascript_importmap_tags "my_engine/application" %>
    – Alex L.
    Commented May 20, 2023 at 14:01
  • Revisiting this now and 2 issues, first is that in the bin/importmap file the #!/usr/bin/env ruby declaration must appear as the first line in the file rather than the file name comment, with this change bin/importmap pin react works the second issue is that the <%= javascript_importmap_tags %> generates an empty import set i.e. "imports": { } No idea why, I am setting this up now to enable me to use stimulus for a add and remove fields in a fieldset feature I'm working on. I have yet to check if stimulus will work but assume that it won't unless the imports set is populated.
    – jamesc
    Commented Sep 24, 2023 at 8:19
  • 1
    Awesome, adding app.config.importmap.paths << root.join("config/importmap.rb") to an initializer in the engine sorted out my issues, thank you again, if you are ever in Leeds I'll have to buy you a pint. No need to add importmap to the dummy test app
    – jamesc
    Commented Sep 26, 2023 at 14:00
  • 1
    @MichaelPell This exercise was not related to turbo although I seem to recall that I did have an issue with Turbo and /I had to disable it for one particular view for reasons that escape me now, sorry, not much help
    – jamesc
    Commented May 1 at 3:10
3

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...

Not the answer you're looking for? Browse other questions tagged or ask your own question.