class Importmap::Map
Constants
- MappedDir
- MappedFile
Attributes
Public Class Methods
Source
# File lib/importmap/map.rb, line 14 def initialize @integrity = false @packages, @directories = {}, {} @cache = {} end
Public Instance Methods
Source
# File lib/importmap/map.rb, line 185 def cache_sweeper(watches: nil) if watches @cache_sweeper = Rails.application.config.file_watcher.new([], Array(watches).collect { |dir| [ dir.to_s, "js"] }.to_h) do clear_cache end else @cache_sweeper end end
Returns an instance of ActiveSupport::EventedFileUpdateChecker configured to clear the cache of the map when the directories passed on initialization via âwatches:` have changes. This is used in development and test to ensure the map caches are reset when javascript files are changed.
Source
# File lib/importmap/map.rb, line 178 def digest(resolver:) Digest::SHA1.hexdigest(to_json(resolver: resolver).to_s) end
Returns a SHA1 digest of the import map json that can be used as a part of a page etag to ensure that a html cache is invalidated when the import map is changed.
Example:
class ApplicationController < ActionController::Base etag { Rails.application.importmap.digest(resolver: helpers) if request.format&.html? } end
Source
# File lib/importmap/map.rb, line 20 def draw(path = nil, &block) if path && File.exist?(path) begin instance_eval(File.read(path), path.to_s) rescue StandardError => e Rails.logger.error "Unable to parse import map from #{path}: #{e.message}" raise InvalidFile, "Unable to parse import map from #{path}: #{e.message}" end elsif block_given? instance_eval(&block) end self end
Source
# File lib/importmap/map.rb, line 67 def enable_integrity! clear_cache @integrity = true end
Enables automatic integrity hash calculation for all pinned modules.
When enabled, integrity values are included in the importmap JSON for all pinned modules. For local assets served by the Rails asset pipeline, integrity hashes are automatically calculated when +integrity: true+ is specified. For modules with explicit integrity values, those values are included as provided. This provides Subresource Integrity (SRI) protection to ensure JavaScript modules havenât been tampered with.
Clears the importmap cache when called to ensure fresh integrity hashes are generated.
Examples
# config/importmap.rb enable_integrity! # These will now auto-calculate integrity hashes pin "application" # integrity: true by default pin "admin", to: "admin.js" # integrity: true by default pin_all_from "app/javascript/lib" # integrity: true by default # Manual control still works pin "no_integrity", integrity: false pin "custom_hash", integrity: "sha384-abc123..."
Notes
-
Integrity calculation is disabled by default and must be explicitly enabled
-
Requires asset pipeline support for integrity calculation (Sprockets or Propshaft 1.2+)
-
For Propshaft, you must configure
config.assets.integrity_hash_algorithm -
External CDN packages should provide their own integrity hashes
Source
# File lib/importmap/map.rb, line 72 def pin(name, to: nil, preload: true, integrity: true) clear_cache @packages[name] = MappedFile.new(name: name, path: to || "#{name}.js", preload: preload, integrity: integrity) end
Source
# File lib/importmap/map.rb, line 77 def pin_all_from(dir, under: nil, to: nil, preload: true, integrity: true) clear_cache @directories[dir] = MappedDir.new(dir: dir, under: under, path: to, preload: preload, integrity: integrity) end
Source
# File lib/importmap/map.rb, line 137 def preloaded_module_packages(resolver:, entry_point: "application", cache_key: :preloaded_module_packages) cache_as(cache_key) do expanded_preloading_packages_and_directories(entry_point:).filter_map do |_, package| resolved_path = resolve_asset_path(package.path, resolver: resolver) next unless resolved_path resolved_integrity = resolve_integrity_value(package.integrity, package.path, resolver: resolver) package = MappedFile.new( name: package.name, path: package.path, preload: package.preload, integrity: resolved_integrity ) [resolved_path, package] end.to_h end end
Returns a hash of resolved module paths to their corresponding package objects for all pinned packages that are marked for preloading. The hash keys are the resolved asset paths, and the values are the MappedFile objects containing package metadata including name, path, preload setting, and integrity.
The resolver must respond to path_to_asset, such as ActionController::Base.helpers or ApplicationController.helpers. Youâll want to use the resolver that has been configured for the asset_host you want these resolved paths to use.
Parameters
resolver-
An object that responds to
path_to_assetfor resolving asset paths. entry_point-
The entry point name or array of entry point names to determine which packages should be preloaded. Defaults to +âapplicationâ+. Packages with +preload: true+ are always included regardless of entry point. Packages with specific entry point names (e.g., +preload: âadminâ+) are only included when that entry point is specified.
cache_key-
A custom cache key to vary the cache used by this method for different cases, such as resolving for different asset hosts. Defaults to
:preloaded_module_packages.
Returns
A hash where:
-
Keys are resolved asset paths (strings)
-
Values are
MappedFileobjects withname,path,preload, andintegrityattributes
Missing assets are gracefully handled and excluded from the returned hash.
Examples
# Get all preloaded packages for the default "application" entry point packages = importmap.preloaded_module_packages(resolver: ApplicationController.helpers) # => { "/assets/application-abc123.js" => #<struct name="application", path="application.js", preload=true, integrity=nil>, # "https://cdn.skypack.dev/react" => #<struct name="react", path="https://cdn.skypack.dev/react", preload=true, integrity="sha384-..."> } # Get preloaded packages for a specific entry point packages = importmap.preloaded_module_packages(resolver: helpers, entry_point: "admin") # Get preloaded packages for multiple entry points packages = importmap.preloaded_module_packages(resolver: helpers, entry_point: ["application", "admin"]) # Use a custom cache key for different asset hosts packages = importmap.preloaded_module_packages(resolver: helpers, cache_key: "cdn_host")
Source
# File lib/importmap/map.rb, line 87 def preloaded_module_paths(resolver:, entry_point: "application", cache_key: :preloaded_module_paths) preloaded_module_packages(resolver: resolver, entry_point: entry_point, cache_key: cache_key).keys end
Returns an array of all the resolved module paths of the pinned packages. The âresolver` must respond to `path_to_asset`, such as `ActionController::Base.helpers` or `ApplicationController.helpers`. Youâll want to use the resolver that has been configured for the âasset_host` you want these resolved paths to use. In case you need to resolve for different asset hosts, you can pass in a custom `cache_key` to vary the cache used by this method for the different cases.
Source
# File lib/importmap/map.rb, line 162 def to_json(resolver:, cache_key: :json) cache_as(cache_key) do packages = expanded_packages_and_directories map = build_import_map(packages, resolver: resolver) JSON.pretty_generate(map) end end
Returns a JSON hash (as a string) of all the resolved module paths of the pinned packages in the import map format. The âresolver` must respond to `path_to_asset`, such as `ActionController::Base.helpers` or `ApplicationController.helpers`. Youâll want to use the resolver that has been configured for the âasset_host` you want these resolved paths to use. In case you need to resolve for different asset hosts, you can pass in a custom `cache_key` to vary the cache used by this method for the different cases.
Private Instance Methods
Source
# File lib/importmap/map.rb, line 316 def absolute_root_of(path) (pathname = Pathname.new(path)).absolute? ? pathname : Rails.root.join(path) end
Source
# File lib/importmap/map.rb, line 235 def build_import_map(packages, resolver:) map = { "imports" => resolve_asset_paths(packages, resolver: resolver) } integrity = build_integrity_hash(packages, resolver: resolver) map["integrity"] = integrity unless integrity.empty? map end
Source
# File lib/importmap/map.rb, line 242 def build_integrity_hash(packages, resolver:) packages.filter_map do |name, mapping| next unless mapping.integrity resolved_path = resolve_asset_path(mapping.path, resolver: resolver) next unless resolved_path integrity_value = resolve_integrity_value(mapping.integrity, mapping.path, resolver: resolver) next unless integrity_value [resolved_path, integrity_value] end.to_h end
Source
# File lib/importmap/map.rb, line 200 def cache_as(name) if result = @cache[name.to_s] result else @cache[name.to_s] = yield end end
Source
# File lib/importmap/map.rb, line 275 def expand_directories_into(paths) @directories.values.each do |mapping| if (absolute_path = absolute_root_of(mapping.dir)).exist? find_javascript_files_in_tree(absolute_path).each do |filename| module_filename = filename.relative_path_from(absolute_path) module_name = module_name_from(module_filename, mapping) module_path = module_path_from(module_filename, mapping) paths[module_name] = MappedFile.new( name: module_name, path: module_path, preload: mapping.preload, integrity: mapping.integrity ) end end end end
Source
# File lib/importmap/map.rb, line 271 def expanded_packages_and_directories @packages.dup.tap { |expanded| expand_directories_into expanded } end
Source
# File lib/importmap/map.rb, line 267 def expanded_preloading_packages_and_directories(entry_point:) expanded_packages_and_directories.select { |name, mapping| mapping.preload.in?([true, false]) ? mapping.preload : (Array(mapping.preload) & Array(entry_point)).any? } end
Source
# File lib/importmap/map.rb, line 312 def find_javascript_files_in_tree(path) Dir[path.join("**/*.js{,m}")].sort.collect { |file| Pathname.new(file) }.select(&:file?) end
Source
# File lib/importmap/map.rb, line 294 def module_name_from(filename, mapping) # Regex explanation: # (?:\/|^) # Matches either / OR the start of the string # index # Matches the word index # $ # Matches the end of the string # # Sample matches # index # folder/index index_regex = /(?:\/|^)index$/ [ mapping.under, filename.to_s.chomp(filename.extname).remove(index_regex).presence ].compact.join("/") end
Source
# File lib/importmap/map.rb, line 308 def module_path_from(filename, mapping) [ mapping.path || mapping.under, filename.to_s ].compact.reject(&:empty?).join("/") end
Source
# File lib/importmap/map.rb, line 212 def rescuable_asset_error?(error) Rails.application.config.importmap.rescuable_asset_errors.any? { |e| error.is_a?(e) } end
Source
# File lib/importmap/map.rb, line 222 def resolve_asset_path(path, resolver:) begin resolver.path_to_asset(path) rescue => e if rescuable_asset_error?(e) Rails.logger.warn "Importmap skipped missing path: #{path}" nil else raise e end end end
Source
# File lib/importmap/map.rb, line 216 def resolve_asset_paths(paths, resolver:) paths.transform_values do |mapping| resolve_asset_path(mapping.path, resolver:) end.compact end
Source
# File lib/importmap/map.rb, line 256 def resolve_integrity_value(integrity, path, resolver:) return unless @integrity case integrity when true resolver.asset_integrity(path) if resolver.respond_to?(:asset_integrity) when String integrity end end