From b97ef073d2d537ba55e70c02e7450845fdae3304 Mon Sep 17 00:00:00 2001 From: Manuel Meurer Date: Sun, 28 Jan 2024 19:59:31 +0100 Subject: [PATCH 1/6] implement multiple importmaps --- .../importmap/importmap_tags_helper.rb | 14 +++++++-- lib/importmap/engine.rb | 30 +++++++++++++++---- lib/importmap/reloader.rb | 4 ++- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/app/helpers/importmap/importmap_tags_helper.rb b/app/helpers/importmap/importmap_tags_helper.rb index c0bb58f..2d52ec7 100644 --- a/app/helpers/importmap/importmap_tags_helper.rb +++ b/app/helpers/importmap/importmap_tags_helper.rb @@ -1,6 +1,14 @@ 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") + entry_point = entry_point.to_s + + importmap_identifier = + entry_point != "application" && Rails.application.importmaps.key?(entry_point) ? + entry_point : + "application" + importmap = Rails.application.importmaps.fetch(entry_point) + safe_join [ javascript_inline_importmap_tag(importmap.to_json(resolver: self)), javascript_importmap_module_preload_tags(importmap), @@ -10,7 +18,7 @@ def javascript_importmap_tags(entry_point = "application", importmap: Rails.appl # 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_json) tag.script importmap_json.html_safe, type: "importmap", "data-turbo-track": "reload", nonce: request&.content_security_policy_nonce end @@ -24,7 +32,7 @@ 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) javascript_module_preload_tag(*importmap.preloaded_module_paths(resolver: self)) end diff --git a/lib/importmap/engine.rb b/lib/importmap/engine.rb index 1c823f4..3de61b1 100755 --- a/lib/importmap/engine.rb +++ b/lib/importmap/engine.rb @@ -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 diff --git a/lib/importmap/reloader.rb b/lib/importmap/reloader.rb index daee235..05c9276 100644 --- a/lib/importmap/reloader.rb +++ b/lib/importmap/reloader.rb @@ -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 From d490b0c4f712ea0e55b1d0d8726745a51d2e8576 Mon Sep 17 00:00:00 2001 From: Manuel Meurer Date: Fri, 2 Feb 2024 08:12:54 +0100 Subject: [PATCH 2/6] fix var name, raise error if importmap for entry_point does not exist --- app/helpers/importmap/importmap_tags_helper.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/helpers/importmap/importmap_tags_helper.rb b/app/helpers/importmap/importmap_tags_helper.rb index 2d52ec7..271c14b 100644 --- a/app/helpers/importmap/importmap_tags_helper.rb +++ b/app/helpers/importmap/importmap_tags_helper.rb @@ -1,13 +1,11 @@ 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") - entry_point = entry_point.to_s + importmap = Rails.application.importmaps.fetch(entry_point.to_s) - importmap_identifier = - entry_point != "application" && Rails.application.importmaps.key?(entry_point) ? - entry_point : - "application" - importmap = Rails.application.importmaps.fetch(entry_point) + unless importmap + raise "No importmap found for entry point '#{entry_point}'." + end safe_join [ javascript_inline_importmap_tag(importmap.to_json(resolver: self)), From 6f5aa5f11f31562eb9a4d932b8f27b1bafd95635 Mon Sep 17 00:00:00 2001 From: Manuel Meurer Date: Fri, 2 Feb 2024 08:13:22 +0100 Subject: [PATCH 3/6] use importmap instead of importmap json as parameter --- app/helpers/importmap/importmap_tags_helper.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/helpers/importmap/importmap_tags_helper.rb b/app/helpers/importmap/importmap_tags_helper.rb index 271c14b..860067a 100644 --- a/app/helpers/importmap/importmap_tags_helper.rb +++ b/app/helpers/importmap/importmap_tags_helper.rb @@ -8,7 +8,7 @@ def javascript_importmap_tags(entry_point = "application") end safe_join [ - javascript_inline_importmap_tag(importmap.to_json(resolver: self)), + javascript_inline_importmap_tag(importmap), javascript_importmap_module_preload_tags(importmap), javascript_import_module_tag(entry_point) ], "\n" @@ -16,7 +16,8 @@ def javascript_importmap_tags(entry_point = "application") # 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) + def javascript_inline_importmap_tag(importmap) + 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 From 91c7feb8b79fe3dd5dc9ee3947c0b18cdf798ddd Mon Sep 17 00:00:00 2001 From: Manuel Meurer Date: Mon, 9 Sep 2024 17:07:40 +0200 Subject: [PATCH 4/6] allow specifying importmap name in helpers --- app/helpers/importmap/importmap_tags_helper.rb | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/app/helpers/importmap/importmap_tags_helper.rb b/app/helpers/importmap/importmap_tags_helper.rb index 860067a..81c6a6e 100644 --- a/app/helpers/importmap/importmap_tags_helper.rb +++ b/app/helpers/importmap/importmap_tags_helper.rb @@ -1,22 +1,17 @@ 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.importmaps.fetch(entry_point.to_s) - - unless importmap - raise "No importmap found for entry point '#{entry_point}'." - end - + def javascript_importmap_tags(entry_point = "application", importmap_name = "application") safe_join [ - javascript_inline_importmap_tag(importmap), - 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) + 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 @@ -31,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) + 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 From 61cfb3a96678669033d0081146ba35a12e23d2db Mon Sep 17 00:00:00 2001 From: Manuel Meurer Date: Mon, 9 Sep 2024 18:06:14 +0200 Subject: [PATCH 5/6] add importmap option to commands, update packager to work with importmap_name, update tests --- lib/importmap/commands.rb | 19 +++++++++++-------- lib/importmap/packager.rb | 5 +++-- test/importmap_tags_helper_test.rb | 10 ++++++---- test/packager_integration_test.rb | 9 ++++++--- test/packager_single_quotes_test.rb | 6 ++++-- test/packager_test.rb | 5 ++++- test/reloader_test.rb | 4 ++-- 7 files changed, 36 insertions(+), 22 deletions(-) diff --git a/lib/importmap/commands.rb b/lib/importmap/commands.rb index 350cc42..3f0a0ad 100644 --- a/lib/importmap/commands.rb +++ b/lib/importmap/commands.rb @@ -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 diff --git a/lib/importmap/packager.rb b/lib/importmap/packager.rb index 76f0661..daaf071 100644 --- a/lib/importmap/packager.rb +++ b/lib/importmap/packager.rb @@ -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 diff --git a/test/importmap_tags_helper_test.rb b/test/importmap_tags_helper_test.rb index 6796aa9..4956c16 100644 --- a/test/importmap_tags_helper_test.rb +++ b/test/importmap_tags_helper_test.rb @@ -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, %{