Class: Importmap::Map

Inherits:
Object
  • Object
show all
Defined in:
lib/importmap/map.rb

Defined Under Namespace

Classes: InvalidFile, MappedDir, MappedFile

Constant Summary collapse

PIN_REGEX =

:nodoc:

/^pin\s+["']([^"']+)["']/.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeMap

Returns a new instance of Map.



14
15
16
17
18
# File 'lib/importmap/map.rb', line 14

def initialize
  @integrity = false
  @packages, @directories = {}, {}
  @cache = {}
end

Instance Attribute Details

#directoriesObject (readonly)

Returns the value of attribute directories.



4
5
6
# File 'lib/importmap/map.rb', line 4

def directories
  @directories
end

#packagesObject (readonly)

Returns the value of attribute packages.



4
5
6
# File 'lib/importmap/map.rb', line 4

def packages
  @packages
end

Class Method Details

.pin_line_regexp_for(package) ⇒ Object

:nodoc:



8
9
10
# File 'lib/importmap/map.rb', line 8

def self.pin_line_regexp_for(package) # :nodoc:
  /^.*pin\s+["']#{Regexp.escape(package)}["'].*$/.freeze
end

Instance Method Details

#cache_sweeper(watches: nil) ⇒ Object

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.



185
186
187
188
189
190
191
192
193
194
# 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

#digest(resolver:) ⇒ Object

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


178
179
180
# File 'lib/importmap/map.rb', line 178

def digest(resolver:)
  Digest::SHA1.hexdigest(to_json(resolver: resolver).to_s)
end

#draw(path = nil, &block) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 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

#enable_integrity!Object

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



67
68
69
70
# File 'lib/importmap/map.rb', line 67

def enable_integrity!
  clear_cache
  @integrity = true
end

#pin(name, to: nil, preload: true, integrity: true) ⇒ Object



72
73
74
75
# 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

#pin_all_from(dir, under: nil, to: nil, preload: true, integrity: true) ⇒ Object



77
78
79
80
# 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

#preloaded_module_packages(resolver:, entry_point: "application", cache_key: :preloaded_module_packages) ⇒ Object

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_asset for 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 MappedFile objects with name, path, preload, and integrity attributes

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")


137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# 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

#preloaded_module_paths(resolver:, entry_point: "application", cache_key: :preloaded_module_paths) ⇒ Object

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.



87
88
89
# 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

#to_json(resolver:, cache_key: :json) ⇒ Object

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.



162
163
164
165
166
167
168
# 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