ruby-on-railspackage-managementimportmap-rails

Rails importmap and gitignore


Rails importmap installs vendor scripts in vendor/javascript and expects me to check that directory into version control:

The packages are downloaded to vendor/javascript, which you can check into your source control, and they'll be available through your application's own asset pipeline serving.

But I don’t want to check those files in. I want to gitignore them, as you normally would with something like NPM and the node_modules directory, and instruct my production environment (Heroku) to install those dependencies itself. Ideally using something I can put in my Procfile.

How do I do that? There’s only one rake task, and that’s to install importmap itself, not the dependencies. I checked the commands too.

I could maybe cobble something together that iterates over the dependencies and downloads them individually, but normally package managers offer a pre-existing public API for this, so I’m sure I’m just missing something.


Solution

  • You're not missing anything, there is no reinstall command. I took the code from this rejected pull request:
    https://github.com/rails/importmap-rails/pull/226

    I had to get creative to make it into a patch, because commands are executed right after the class definition:

    # config/application.rb
    
    def Importmap.const_added(const_name)
      if const_name == :Commands
        Importmap::Commands.class_eval do
          desc "redownload", "Force download every package, even if the required versions are already downloaded"
          def redownload
            packages = npm.packages_with_versions.map { |package| package.join("@") }
            pin packages
          end
        end
      end
    end
    
    $ bin/importmap redownload
    Pinning "@popperjs/core" to vendor/javascript/@popperjs/core.js via download from https://ga.jspm.io/npm:@popperjs/core@2.11.8/lib/index.js
    Pinning "bootstrap" to vendor/javascript/bootstrap.js via download from https://ga.jspm.io/npm:bootstrap@5.3.3/dist/js/bootstrap.esm.js
    

    Update

    If you have packages pinned to urls, you could just start downloading them instead which is the default behavior in importmap-rails v2, otherwise, you have to do some manual work:

    def redownload
      # packages = npm.packages_with_versions.map { |package| package.join("@") }
      packages = npm.send(:importmap).scan(/^pin ["']([^["']]*)["'].* #.*@(\d+\.\d+\.\d+(?:[^\s]*)).*$/).map{_1*"@"}
      pin packages
    end
    

    https://github.com/rails/importmap-rails/blob/v2.0.1/lib/importmap/npm.rb#L52


    Update #2

    Looks like the source of the download is not tracked anywhere. I've just checked bin/importmap update command and that also ignores the original source and uses jspm, which seems like a bug to me.

    This one is a bit more involved. First, add ability to track the original source:

    # config/application.rb
    
    require "importmap/packager"
    Importmap::Packager.class_eval do
      # https://github.com/rails/importmap-rails/blob/v2.0.1/lib/importmap/packager.rb#L39
      def vendored_pin_for(package, url)
        filename = package_filename(package)
        version  = extract_package_version_from(url)
        vendor   = url.match(/npm|skypack|unpkg|jsdelivr/) # <= this
    
        if "#{package}.js" == filename
          %(pin "#{package}" # #{version} #{vendor}).strip
        else
          %(pin "#{package}", to: "#{filename}" # #{version} #{vendor}).strip
        end
      end
    end
    

    if you pin imask:

    bin/importmap pin imask --from unpkg
    

    you get a pin like this:

    pin "imask" # @7.6.1 unpkg
    

    Now parse this importmap and use unpkg as a --from option:

    # config/application.rb
    
    def Importmap.const_added(const_name)
      if const_name == :Commands
        Importmap::Commands.class_eval do
          desc "redownload", "Force download every package, even if the required versions are already downloaded"
          def redownload
            #                                                                     adjusted here vvvvvvv
            npm.send(:importmap).scan(/^pin ["']([^["']]*)["'].* #.*@(\d+\.\d+\.\d+(?:[^\s]*)).*?(\w+)?$/).group_by { _3 }.each do |from, packages|
              packages_with_version = packages.map do |package|
                package.pop
                package * "@"
              end
    
              @options = {from:}
              pin packages_with_version
            end
          end
        end
      end
    end
    

    Test:

    # config/importmap.rb
    
    pin "bootstrap" # @5.3.3
    pin "@popperjs/core", to: "@popperjs--core.js" # @2.11.8
    pin "jquery" # @3.7.1 jsdelivr
    pin "imask" # @7.6.1 unpkg
    
    $ bin/importmap redownload
    
    Pinning "@popperjs/core" to vendor/javascript/@popperjs/core.js via download from https://ga.jspm.io/npm:@popperjs/core@2.11.8/lib/index.js
    Pinning "bootstrap" to vendor/javascript/bootstrap.js via download from https://ga.jspm.io/npm:bootstrap@5.3.3/dist/js/bootstrap.esm.js
    Pinning "jquery" to vendor/javascript/jquery.js via download from https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.js
    Pinning "imask" to vendor/javascript/imask.js via download from https://unpkg.com/imask@7.6.1/esm/index.js