require"pathname"classImportmap::Mapattr_reader:packages,:directoriesclassInvalidFile<StandardError;enddefinitialize@packages,@directories={},{}@cache={}enddefdraw(path=nil,&block)ifpath&&File.exist?(path)begininstance_eval(File.read(path),path.to_s)rescueStandardError=>eRails.logger.error"Unable to parse import map from #{path}: #{e.message}"raiseInvalidFile,"Unable to parse import map from #{path}: #{e.message}"endelsifblock_given?instance_eval(&block)endselfenddefpin(name,to: nil,preload: true)clear_cache@packages[name]=MappedFile.new(name: name,path: to||"#{name}.js",preload: preload)enddefpin_all_from(dir,under: nil,to: nil,preload: true)clear_cache@directories[dir]=MappedDir.new(dir: dir,under: under,path: to,preload: preload)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.defpreloaded_module_paths(resolver:,entry_point: "application",cache_key: :preloaded_module_paths)cache_as(cache_key)doresolve_asset_paths(expanded_preloading_packages_and_directories(entry_point:),resolver:).valuesendend# 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.defto_json(resolver:,cache_key: :json)cache_as(cache_key)doJSON.pretty_generate({"imports"=>resolve_asset_paths(expanded_packages_and_directories,resolver: resolver)})endend# 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? }# enddefdigest(resolver:)Digest::SHA1.hexdigest(to_json(resolver: resolver).to_s)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.defcache_sweeper(watches: nil)ifwatches@cache_sweeper=Rails.application.config.file_watcher.new([],Array(watches).collect{|dir|[dir.to_s,"js"]}.to_h)doclear_cacheendelse@cache_sweeperendendprivateMappedDir=Struct.new(:dir,:path,:under,:preload,keyword_init: true)MappedFile=Struct.new(:name,:path,:preload,keyword_init: true)defcache_as(name)ifresult=@cache[name.to_s]resultelse@cache[name.to_s]=yieldendenddefclear_cache@cache.clearenddefrescuable_asset_error?(error)Rails.application.config.importmap.rescuable_asset_errors.any?{|e|error.is_a?(e)}enddefresolve_asset_paths(paths,resolver:)paths.transform_valuesdo|mapping|beginresolver.path_to_asset(mapping.path)rescue=>eifrescuable_asset_error?(e)Rails.logger.warn"Importmap skipped missing path: #{mapping.path}"nilelseraiseeendendend.compactenddefexpanded_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?}enddefexpanded_packages_and_directories@packages.dup.tap{|expanded|expand_directories_intoexpanded}enddefexpand_directories_into(paths)@directories.values.eachdo|mapping|if(absolute_path=absolute_root_of(mapping.dir)).exist?find_javascript_files_in_tree(absolute_path).eachdo|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)endendendenddefmodule_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/indexindex_regex=/(?:\/|^)index$/[mapping.under,filename.to_s.remove(filename.extname).remove(index_regex).presence].compact.join("/")enddefmodule_path_from(filename,mapping)[mapping.path||mapping.under,filename.to_s].compact.reject(&:empty?).join("/")enddeffind_javascript_files_in_tree(path)Dir[path.join("**/*.js{,m}")].sort.collect{|file|Pathname.new(file)}.select(&:file?)enddefabsolute_root_of(path)(pathname=Pathname.new(path)).absolute??pathname:Rails.root.join(path)endend