class Importmap::Map
def absolute_root_of(path)
def absolute_root_of(path) (pathname = Pathname.new(path)).absolute? ? pathname : Rails.root.join(path) end
def cache_as(name)
def cache_as(name) if result = @cache[name.to_s] result else @cache[name.to_s] = yield end end
def cache_sweeper(watches: nil)
when the directories passed on initialization via `watches:` have changes. This is used in development
Returns an instance of ActiveSupport::EventedFileUpdateChecker configured to clear the cache of the map
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
def clear_cache
def clear_cache @cache.clear end
def digest(resolver:)
etag { Rails.application.importmap.digest(resolver: helpers) if request.format&.html? }
class ApplicationController < ActionController::Base
Example:
ensure that a html cache is invalidated when the import map is changed.
Returns a SHA1 digest of the import map json that can be used as a part of a page etag to
def digest(resolver:) Digest::SHA1.hexdigest(to_json(resolver: resolver).to_s) end
def draw(path = nil, &block)
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
def expand_directories_into(paths)
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) end end end end
def expanded_packages_and_directories
def expanded_packages_and_directories @packages.dup.tap { |expanded| expand_directories_into expanded } end
def expanded_preloading_packages_and_directories(entry_point:)
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
def find_javascript_files_in_tree(path)
def find_javascript_files_in_tree(path) Dir[path.join("**/*.js{,m}")].sort.collect { |file| Pathname.new(file) }.select(&:file?) end
def initialize
def initialize @packages, @directories = {}, {} @cache = {} end
def module_name_from(filename, mapping)
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.remove(filename.extname).remove(index_regex).presence ].compact.join("/") end
def module_path_from(filename, mapping)
def module_path_from(filename, mapping) [ mapping.path || mapping.under, filename.to_s ].compact.reject(&:empty?).join("/") end
def pin(name, to: nil, preload: true)
def pin(name, to: nil, preload: true) clear_cache @packages[name] = MappedFile.new(name: name, path: to || "#{name}.js", preload: preload) end
def pin_all_from(dir, under: nil, to: nil, preload: true)
def pin_all_from(dir, under: nil, to: nil, preload: true) clear_cache @directories[dir] = MappedDir.new(dir: dir, under: under, path: to, preload: preload) end
def preloaded_module_paths(resolver:, entry_point: "application", cache_key: :preloaded_module_paths)
resolve for different asset hosts, you can pass in a custom `cache_key` to vary the cache used by this method for
resolver that has been configured for the `asset_host` you want these resolved paths to use. In case you need to
`path_to_asset`, such as `ActionController::Base.helpers` or `ApplicationController.helpers`. You'll want to use the
Returns an array of all the resolved module paths of the pinned packages. The `resolver` must respond to
def preloaded_module_paths(resolver:, entry_point: "application", cache_key: :preloaded_module_paths) cache_as(cache_key) do resolve_asset_paths(expanded_preloading_packages_and_directories(entry_point:), resolver:).values end end
def rescuable_asset_error?(error)
def rescuable_asset_error?(error) Rails.application.config.importmap.rescuable_asset_errors.any? { |e| error.is_a?(e) } end
def resolve_asset_paths(paths, resolver:)
def resolve_asset_paths(paths, resolver:) paths.transform_values do |mapping| begin resolver.path_to_asset(mapping.path) rescue => e if rescuable_asset_error?(e) Rails.logger.warn "Importmap skipped missing path: #{mapping.path}" nil else raise e end end end.compact end
def to_json(resolver:, cache_key: :json)
want these resolved paths to use. In case you need to resolve for different asset hosts, you can pass in a custom
`ApplicationController.helpers`. You'll want to use the resolver that has been configured for the `asset_host` you
The `resolver` must respond to `path_to_asset`, such as `ActionController::Base.helpers` or
Returns a JSON hash (as a string) of all the resolved module paths of the pinned packages in the import map format.
def to_json(resolver:, cache_key: :json) cache_as(cache_key) do JSON.pretty_generate({ "imports" => resolve_asset_paths(expanded_packages_and_directories, resolver: resolver) }) end end