Skip to content

Allow multiple importmaps in config/importmaps/ #241

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 6 commits into from
Closed
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
13 changes: 8 additions & 5 deletions app/helpers/importmap/importmap_tags_helper.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
module Importmap::ImportmapTagsHelper
# Setup all script tags needed to use an importmap-powered entrypoint (which defaults to application.js)
def javascript_importmap_tags(entry_point = "application", importmap: Rails.application.importmap)
def javascript_importmap_tags(entry_point = "application", importmap_name = "application")
safe_join [
javascript_inline_importmap_tag(importmap.to_json(resolver: self)),
javascript_importmap_module_preload_tags(importmap),
javascript_inline_importmap_tag(importmap_name),
javascript_importmap_module_preload_tags(importmap_name),
javascript_import_module_tag(entry_point)
], "\n"
end

# Generate an inline importmap tag using the passed `importmap_json` JSON string.
# By default, `Rails.application.importmap.to_json(resolver: self)` is used.
def javascript_inline_importmap_tag(importmap_json = Rails.application.importmap.to_json(resolver: self))
def javascript_inline_importmap_tag(importmap_name = "application")
importmap = Rails.application.importmaps.fetch(importmap_name.to_s)
importmap_json = importmap.to_json(resolver: self)
tag.script importmap_json.html_safe,
type: "importmap", "data-turbo-track": "reload", nonce: request&.content_security_policy_nonce
end
@@ -24,7 +26,8 @@ def javascript_import_module_tag(*module_names)
# Link tags for preloading all modules marked as preload: true in the `importmap`
# (defaults to Rails.application.importmap), such that they'll be fetched
# in advance by browsers supporting this link type (https://caniuse.com/?search=modulepreload).
def javascript_importmap_module_preload_tags(importmap = Rails.application.importmap)
def javascript_importmap_module_preload_tags(importmap_name = "application")
importmap = Rails.application.importmaps.fetch(importmap_name.to_s)
javascript_module_preload_tag(*importmap.preloaded_module_paths(resolver: self))
end

2 changes: 1 addition & 1 deletion gemfiles/rails_main_propshaft.gemfile
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
source "https://rubygems.org"

gem "rails", branch: "main", git: "https://github.com/rails/rails.git"
gem "sqlite3", "~> 1.4"
gem "sqlite3", "~> 2.0"
gem "propshaft"

group :development do
2 changes: 1 addition & 1 deletion gemfiles/rails_main_sprockets.gemfile
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
source "https://rubygems.org"

gem "rails", branch: "main", git: "https://github.com/rails/rails.git"
gem "sqlite3", "~> 1.4"
gem "sqlite3", "~> 2.0"
gem "sprockets-rails"

group :development do
19 changes: 11 additions & 8 deletions lib/importmap/commands.rb
Original file line number Diff line number Diff line change
@@ -12,17 +12,20 @@ def self.exit_on_failure?
desc "pin [*PACKAGES]", "Pin new packages"
option :env, type: :string, aliases: :e, default: "production"
option :from, type: :string, aliases: :f, default: "jspm"
option :importmap, type: :string, aliases: :i, default: "application"
def pin(*packages)
packager = Importmap::Packager.new(options[:importmap])

if imports = packager.import(*packages, env: options[:env], from: options[:from])
imports.each do |package, url|
puts %(Pinning "#{package}" to #{packager.vendor_path}/#{package}.js via download from #{url})
puts %(Pinning "#{package}" to #{packager.vendor_path}/#{package}.js via download from #{url} in importmap "#{options[:importmap]}")
packager.download(package, url)
pin = packager.vendored_pin_for(package, url)

if packager.packaged?(package)
gsub_file("config/importmap.rb", /^pin "#{package}".*$/, pin, verbose: false)
gsub_file(packager.importmap_path, /^pin "#{package}".*$/, pin, verbose: false)
else
append_to_file("config/importmap.rb", "#{pin}\n", verbose: false)
append_to_file(packager.importmap_path, "#{pin}\n", verbose: false)
end
end
else
@@ -33,7 +36,10 @@ def pin(*packages)
desc "unpin [*PACKAGES]", "Unpin existing packages"
option :env, type: :string, aliases: :e, default: "production"
option :from, type: :string, aliases: :f, default: "jspm"
option :importmap, type: :string, aliases: :i, default: "application"
def unpin(*packages)
packager = Importmap::Packager.new(options[:importmap])

if imports = packager.import(*packages, env: options[:env], from: options[:from])
imports.each do |package, url|
if packager.packaged?(package)
@@ -47,9 +53,10 @@ def unpin(*packages)
end

desc "json", "Show the full importmap in json"
option :importmap, type: :string, aliases: :i, default: "application"
def json
require Rails.root.join("config/environment")
puts Rails.application.importmap.to_json(resolver: ActionController::Base.helpers)
puts Rails.application.importmaps[options[:importmap]].to_json(resolver: ActionController::Base.helpers)
end

desc "audit", "Run a security audit"
@@ -104,10 +111,6 @@ def packages
end

private
def packager
@packager ||= Importmap::Packager.new
end

def npm
@npm ||= Importmap::Npm.new
end
30 changes: 24 additions & 6 deletions lib/importmap/engine.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require "importmap/map"

# Use Rails.application.importmap to access the map
Rails::Application.send(:attr_accessor, :importmap)
# Use Rails.application.importmaps to access the maps
Rails::Application.send(:attr_accessor, :importmaps)

module Importmap
class Engine < ::Rails::Engine
@@ -14,9 +14,20 @@ class Engine < ::Rails::Engine
config.autoload_once_paths = %W( #{root}/app/helpers )

initializer "importmap" do |app|
app.importmap = Importmap::Map.new
importmap = Importmap::Map.new
app.importmaps = {
"application" => importmap
}
app.config.importmap.paths << app.root.join("config/importmap.rb")
app.config.importmap.paths.each { |path| app.importmap.draw(path) }
app.config.importmap.paths.each { |path| importmap.draw(path) }

Dir[app.root.join("config/importmaps/*.rb")].each do |path|
namespace = File.basename(path).delete_suffix(".rb")
importmap = Importmap::Map.new
app.config.importmap.paths.each { |path| importmap.draw(path) }
importmap.draw(path)
app.importmaps[namespace] = importmap
end
end

initializer "importmap.reloader" do |app|
@@ -33,10 +44,17 @@ class Engine < ::Rails::Engine
if app.config.importmap.sweep_cache && !app.config.cache_classes
app.config.importmap.cache_sweepers << app.root.join("app/javascript")
app.config.importmap.cache_sweepers << app.root.join("vendor/javascript")
app.importmap.cache_sweeper(watches: app.config.importmap.cache_sweepers)

app.importmaps.values.each do |importmap|
importmap.cache_sweeper(watches: app.config.importmap.cache_sweepers)
end

ActiveSupport.on_load(:action_controller_base) do
before_action { Rails.application.importmap.cache_sweeper.execute_if_updated }
before_action do
Rails.application.importmaps.values.each do |importmap|
importmap.cache_sweeper.execute_if_updated
end
end
end
end
end
5 changes: 3 additions & 2 deletions lib/importmap/packager.rb
Original file line number Diff line number Diff line change
@@ -10,10 +10,11 @@ class Importmap::Packager
singleton_class.attr_accessor :endpoint
self.endpoint = URI("https://api.jspm.io/generate")

attr_reader :importmap_path
attr_reader :vendor_path

def initialize(importmap_path = "config/importmap.rb", vendor_path: "vendor/javascript")
@importmap_path = Pathname.new(importmap_path)
def initialize(importmap_name = "application", vendor_path: "vendor/javascript")
@importmap_path = Pathname.new(importmap_name == "application" ? "config/importmap.rb" : "config/importmaps/#{importmap_name}.rb")
@vendor_path = Pathname.new(vendor_path)
end

4 changes: 3 additions & 1 deletion lib/importmap/reloader.rb
Original file line number Diff line number Diff line change
@@ -5,7 +5,9 @@ class Importmap::Reloader
delegate :execute_if_updated, :execute, :updated?, to: :updater

def reload!
import_map_paths.each { |path| Rails.application.importmap.draw(path) }
Rails.application.importmaps.values.each do |importmap|
import_map_paths.each { |path| importmap.draw(path) }
end
end

private
10 changes: 6 additions & 4 deletions test/importmap_tags_helper_test.rb
Original file line number Diff line number Diff line change
@@ -50,10 +50,12 @@ def content_security_policy_nonce
end

test "using a custom importmap" do
importmap = Importmap::Map.new
importmap.pin "foo", preload: true
importmap.pin "bar", preload: false
importmap_html = javascript_importmap_tags("foo", importmap: importmap)
Rails.application.importmaps["custom"] = Importmap::Map.new.tap do |importmap|
importmap.pin "foo", preload: true
importmap.pin "bar", preload: false
end

importmap_html = javascript_importmap_tags("foo", "custom")

assert_includes importmap_html, %{<script type="importmap" data-turbo-track="reload">}
assert_includes importmap_html, %{"foo": "/foo.js"}
9 changes: 6 additions & 3 deletions test/packager_integration_test.rb
Original file line number Diff line number Diff line change
@@ -2,7 +2,10 @@
require "importmap/packager"

class Importmap::PackagerIntegrationTest < ActiveSupport::TestCase
setup { @packager = Importmap::Packager.new(Rails.root.join("config/importmap.rb")) }
setup do
Dir.chdir(Rails.root)
@packager = Importmap::Packager.new
end

test "successful import against live service" do
assert_equal "https://ga.jspm.io/npm:react@17.0.2/index.js", @packager.import("react@17.0.2")["react"]
@@ -26,7 +29,7 @@ class Importmap::PackagerIntegrationTest < ActiveSupport::TestCase
test "successful downloads from live service" do
Dir.mktmpdir do |vendor_dir|
@packager = Importmap::Packager.new \
Rails.root.join("config/importmap.rb"),
"application",
vendor_path: Pathname.new(vendor_dir)

package_url = "https://ga.jspm.io/npm:@github/webauthn-json@0.5.7/dist/main/webauthn-json.js"
@@ -40,7 +43,7 @@ class Importmap::PackagerIntegrationTest < ActiveSupport::TestCase
@packager.download("react", package_url)
assert File.exist?(vendored_package_file)
assert_equal "// react@17.0.2 downloaded from #{package_url}", File.readlines(vendored_package_file).first.strip

@packager.remove("react")
assert_not File.exist?(Pathname.new(vendor_dir).join("react.js"))
end
6 changes: 4 additions & 2 deletions test/packager_single_quotes_test.rb
Original file line number Diff line number Diff line change
@@ -3,9 +3,11 @@

class Importmap::PackagerSingleQuotesTest < ActiveSupport::TestCase
setup do
@single_quote_config_name = Rails.root.join("config/importmap_with_single_quotes.rb")
@single_quote_config_name = Rails.root.join("config/importmaps/single_quotes.rb")
FileUtils.mkdir_p @single_quote_config_name.dirname
File.write(@single_quote_config_name, File.read(Rails.root.join("config/importmap.rb")).tr('"', "'"))
@packager = Importmap::Packager.new(@single_quote_config_name)
Dir.chdir(Rails.root)
@packager = Importmap::Packager.new("single_quotes")
end

teardown { File.delete(@single_quote_config_name) }
5 changes: 4 additions & 1 deletion test/packager_test.rb
Original file line number Diff line number Diff line change
@@ -3,7 +3,10 @@
require "minitest/mock"

class Importmap::PackagerTest < ActiveSupport::TestCase
setup { @packager = Importmap::Packager.new(Rails.root.join("config/importmap.rb")) }
setup do
Dir.chdir(Rails.root)
@packager = Importmap::Packager.new
end

test "successful import with mock" do
response = Class.new do
4 changes: 2 additions & 2 deletions test/reloader_test.rb
Original file line number Diff line number Diff line change
@@ -13,10 +13,10 @@ class ReloaderTest < ActiveSupport::TestCase
end

test "redraws importmap when config changes" do
Rails.application.importmap = Importmap::Map.new.draw { pin "md5", to: "https://cdn.skypack.dev/md5" }
Rails.application.importmaps["application"] = Importmap::Map.new.draw { pin "md5", to: "https://cdn.skypack.dev/md5" }
assert_not_predicate @reloader, :updated?

assert_changes -> { Rails.application.importmap.packages.keys }, from: %w[ md5 ], to: %w[ md5 not_there ] do
assert_changes -> { Rails.application.importmaps["application"].packages.keys }, from: %w[ md5 ], to: %w[ md5 not_there ] do
touch_config
assert @reloader.execute_if_updated
end