importmap-rails 2.2.1 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c286a2385b19e8a170d991478ce228cfb1c0e27eb02fbda6349a889798caa85f
4
- data.tar.gz: a40cea607756433bf36ad49e291e342f324a303c80e0286e980ee27c6c9001f9
3
+ metadata.gz: 91837e96107f59efa73a314dec3955a488f9f40826c27e116da43c5d9ef0a44a
4
+ data.tar.gz: ce9d44539cd6bb86b1e027c9c714dc8274b4a7b918ee0a67493b79b9eb58103f
5
5
  SHA512:
6
- metadata.gz: 71fc6847c3860c86d611dad36554feab750059128eee5d72961da0cd6bc2ba6ec08dc96358bb7919f487243d7463ca304efbbbf68874d7b58c9b85580d652f75
7
- data.tar.gz: 59b0d5ce601ef182abd5f1017f414eec8fea35d1e48e270b0edd10bcf0c59cd7c5e52c1af0dab4f98f0ea81972ce82004af3011d1f3399d152ce077fbbd7a7e1
6
+ metadata.gz: 95a4d846c8a808b8037e2425ad307e1cecd736051053e86f5c9739f9118375d81e0e967f64be3f6cee916d0ce7ca763fd7b236684f9b0889679b62f5718643a0
7
+ data.tar.gz: 5e20ed5b2d79945a603db1cf6e0e5d694a8a7ad7b96f5bb13e6e1083225034fcbf7ba2019acf397b004345bd4d54827895b9e4ffd5c831eb1fdfc2c051dcb063
data/README.md CHANGED
@@ -81,12 +81,13 @@ If you want to import local js module files from `app/javascript/src` or other s
81
81
  pin_all_from 'app/javascript/src', under: 'src', to: 'src'
82
82
 
83
83
  # With automatic integrity calculation for enhanced security
84
+ enable_integrity!
84
85
  pin_all_from 'app/javascript/controllers', under: 'controllers', integrity: true
85
86
  ```
86
87
 
87
88
  The `:to` parameter is only required if you want to change the destination logical import name. If you drop the :to option, you must place the :under option directly after the first parameter.
88
89
 
89
- The `integrity: true` option automatically calculates integrity hashes for all files in the directory, providing security benefits without manual hash management.
90
+ The `enable_integrity!` call enables integrity calculation globally, and `integrity: true` automatically calculates integrity hashes for all files in the directory, providing security benefits without manual hash management.
90
91
 
91
92
  Allows you to:
92
93
 
@@ -142,12 +143,15 @@ For enhanced security, importmap-rails supports [Subresource Integrity (SRI)](ht
142
143
 
143
144
  ### Automatic integrity for local assets
144
145
 
145
- Starting with importmap-rails, **`integrity: true` is the default** for all pins. This automatically calculates integrity hashes for local assets served by the Rails asset pipeline:
146
+ To enable automatic integrity calculation for local assets served by the Rails asset pipeline, you must first call `enable_integrity!` in your importmap configuration:
146
147
 
147
148
  ```ruby
148
149
  # config/importmap.rb
149
150
 
150
- # These all use integrity: true by default
151
+ # Enable integrity calculation globally
152
+ enable_integrity!
153
+
154
+ # With integrity enabled, these will auto-calculate integrity hashes
151
155
  pin "application" # Auto-calculated integrity
152
156
  pin "admin", to: "admin.js" # Auto-calculated integrity
153
157
  pin_all_from "app/javascript/controllers", under: "controllers" # Auto-calculated integrity
@@ -163,7 +167,7 @@ This is particularly useful for:
163
167
  * **Bulk operations** with `pin_all_from` where calculating hashes manually would be tedious
164
168
  * **Development workflow** where asset contents change frequently
165
169
 
166
- This behavior can be disabled by setting `integrity: false` or `integrity: nil`
170
+ **Note:** Integrity calculation is opt-in and must be enabled with `enable_integrity!`. This behavior can be further controlled by setting `integrity: false` or `integrity: nil` on individual pins.
167
171
 
168
172
  **Important for Propshaft users:** SRI support requires Propshaft 1.2+ and you must configure the integrity hash algorithm in your application:
169
173
 
@@ -174,7 +178,7 @@ config.assets.integrity_hash_algorithm = 'sha256' # or 'sha384', 'sha512'
174
178
 
175
179
  Without this configuration, integrity will be disabled by default when using Propshaft. Sprockets includes integrity support out of the box.
176
180
 
177
- **Example output with `integrity: true`:**
181
+ **Example output with `enable_integrity!` and `integrity: true`:**
178
182
  ```json
179
183
  {
180
184
  "imports": {
@@ -15,13 +15,7 @@ class Importmap::Commands < Thor
15
15
  option :preload, type: :string, repeatable: true, desc: "Can be used multiple times"
16
16
  def pin(*packages)
17
17
  for_each_import(packages, env: options[:env], from: options[:from]) do |package, url|
18
- puts %(Pinning "#{package}" to #{packager.vendor_path}/#{package}.js via download from #{url})
19
-
20
- packager.download(package, url)
21
-
22
- pin = packager.vendored_pin_for(package, url, options[:preload])
23
-
24
- update_importmap_with_pin(package, pin)
18
+ pin_package(package, url, options[:preload])
25
19
  end
26
20
  end
27
21
 
@@ -96,7 +90,14 @@ class Importmap::Commands < Thor
96
90
  desc "update", "Update outdated package pins"
97
91
  def update
98
92
  if (outdated_packages = npm.outdated_packages).any?
99
- pin(*outdated_packages.map(&:name))
93
+ package_names = outdated_packages.map(&:name)
94
+ packages_with_options = packager.extract_existing_pin_options(package_names)
95
+
96
+ for_each_import(package_names, env: "production", from: "jspm") do |package, url|
97
+ options = packages_with_options[package] || {}
98
+
99
+ pin_package(package, url, options[:preload])
100
+ end
100
101
  else
101
102
  puts "No outdated packages found"
102
103
  end
@@ -116,11 +117,23 @@ class Importmap::Commands < Thor
116
117
  @npm ||= Importmap::Npm.new
117
118
  end
118
119
 
120
+ def pin_package(package, url, preload)
121
+ puts %(Pinning "#{package}" to #{packager.vendor_path}/#{package}.js via download from #{url})
122
+
123
+ packager.download(package, url)
124
+
125
+ pin = packager.vendored_pin_for(package, url, preload)
126
+
127
+ update_importmap_with_pin(package, pin)
128
+ end
129
+
119
130
  def update_importmap_with_pin(package, pin)
131
+ new_pin = "#{pin}\n"
132
+
120
133
  if packager.packaged?(package)
121
- gsub_file("config/importmap.rb", /^pin "#{package}".*$/, pin, verbose: false)
134
+ gsub_file("config/importmap.rb", Importmap::Map.pin_line_regexp_for(package), pin, verbose: false)
122
135
  else
123
- append_to_file("config/importmap.rb", "#{pin}\n", verbose: false)
136
+ append_to_file("config/importmap.rb", new_pin, verbose: false)
124
137
  end
125
138
  end
126
139
 
data/lib/importmap/map.rb CHANGED
@@ -3,9 +3,16 @@ require "pathname"
3
3
  class Importmap::Map
4
4
  attr_reader :packages, :directories
5
5
 
6
+ PIN_REGEX = /^pin\s+["']([^"']+)["']/.freeze # :nodoc:
7
+
8
+ def self.pin_line_regexp_for(package) # :nodoc:
9
+ /^.*pin\s+["']#{Regexp.escape(package)}["'].*$/.freeze
10
+ end
11
+
6
12
  class InvalidFile < StandardError; end
7
13
 
8
14
  def initialize
15
+ @integrity = false
9
16
  @packages, @directories = {}, {}
10
17
  @cache = {}
11
18
  end
@@ -25,6 +32,43 @@ class Importmap::Map
25
32
  self
26
33
  end
27
34
 
35
+ # Enables automatic integrity hash calculation for all pinned modules.
36
+ #
37
+ # When enabled, integrity values are included in the importmap JSON for all
38
+ # pinned modules. For local assets served by the Rails asset pipeline,
39
+ # integrity hashes are automatically calculated when +integrity: true+ is
40
+ # specified. For modules with explicit integrity values, those values are
41
+ # included as provided. This provides Subresource Integrity (SRI) protection
42
+ # to ensure JavaScript modules haven't been tampered with.
43
+ #
44
+ # Clears the importmap cache when called to ensure fresh integrity hashes
45
+ # are generated.
46
+ #
47
+ # ==== Examples
48
+ #
49
+ # # config/importmap.rb
50
+ # enable_integrity!
51
+ #
52
+ # # These will now auto-calculate integrity hashes
53
+ # pin "application" # integrity: true by default
54
+ # pin "admin", to: "admin.js" # integrity: true by default
55
+ # pin_all_from "app/javascript/lib" # integrity: true by default
56
+ #
57
+ # # Manual control still works
58
+ # pin "no_integrity", integrity: false
59
+ # pin "custom_hash", integrity: "sha384-abc123..."
60
+ #
61
+ # ==== Notes
62
+ #
63
+ # * Integrity calculation is disabled by default and must be explicitly enabled
64
+ # * Requires asset pipeline support for integrity calculation (Sprockets or Propshaft 1.2+)
65
+ # * For Propshaft, you must configure +config.assets.integrity_hash_algorithm+
66
+ # * External CDN packages should provide their own integrity hashes
67
+ def enable_integrity!
68
+ clear_cache
69
+ @integrity = true
70
+ end
71
+
28
72
  def pin(name, to: nil, preload: true, integrity: true)
29
73
  clear_cache
30
74
  @packages[name] = MappedFile.new(name: name, path: to || "#{name}.js", preload: preload, integrity: integrity)
@@ -210,6 +254,8 @@ class Importmap::Map
210
254
  end
211
255
 
212
256
  def resolve_integrity_value(integrity, path, resolver:)
257
+ return unless @integrity
258
+
213
259
  case integrity
214
260
  when true
215
261
  resolver.asset_integrity(path) if resolver.respond_to?(:asset_integrity)
data/lib/importmap/npm.rb CHANGED
@@ -3,7 +3,7 @@ require "uri"
3
3
  require "json"
4
4
 
5
5
  class Importmap::Npm
6
- PIN_REGEX = /^pin ["']([^["']]*)["'].*/
6
+ PIN_REGEX = /#{Importmap::Map::PIN_REGEX}.*/.freeze # :nodoc:
7
7
 
8
8
  Error = Class.new(StandardError)
9
9
  HTTPError = Class.new(Error)
@@ -17,7 +17,7 @@ class Importmap::Npm
17
17
  end
18
18
 
19
19
  def outdated_packages
20
- packages_with_versions.each.with_object([]) do |(package, current_version), outdated_packages|
20
+ packages_with_versions.each_with_object([]) do |(package, current_version), outdated_packages|
21
21
  outdated_package = OutdatedPackage.new(name: package, current_version: current_version)
22
22
 
23
23
  if !(response = get_package(package))
@@ -51,7 +51,7 @@ class Importmap::Npm
51
51
  def packages_with_versions
52
52
  # We cannot use the name after "pin" because some dependencies are loaded from inside packages
53
53
  # Eg. pin "buffer", to: "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.19/nodelibs/browser/buffer.js"
54
- with_versions = importmap.scan(/^pin .*(?<=npm:|npm\/|skypack\.dev\/|unpkg\.com\/)(.*)(?=@\d+\.\d+\.\d+)@(\d+\.\d+\.\d+(?:[^\/\s["']]*)).*$/) |
54
+ with_versions = importmap.scan(/^pin .*(?<=npm:|npm\/|skypack\.dev\/|unpkg\.com\/)([^@\/]+)@(\d+\.\d+\.\d+(?:[^\/\s"']*))/) |
55
55
  importmap.scan(/#{PIN_REGEX} #.*@(\d+\.\d+\.\d+(?:[^\s]*)).*$/)
56
56
 
57
57
  vendored_packages_without_version(with_versions).each do |package, path|
@@ -147,7 +147,7 @@ class Importmap::Npm
147
147
  end
148
148
 
149
149
  def find_unversioned_vendored_package(line, versioned_packages)
150
- regexp = line.include?("to:")? /#{PIN_REGEX}to: ["']([^["']]*)["'].*/ : PIN_REGEX
150
+ regexp = line.include?("to:")? /#{PIN_REGEX}to: ["']([^"']*)["'].*/ : PIN_REGEX
151
151
  match = line.match(regexp)
152
152
 
153
153
  return unless match
@@ -3,6 +3,9 @@ require "uri"
3
3
  require "json"
4
4
 
5
5
  class Importmap::Packager
6
+ PIN_REGEX = /#{Importmap::Map::PIN_REGEX}(.*)/.freeze # :nodoc:
7
+ PRELOAD_OPTION_REGEXP = /preload:\s*(\[[^\]]+\]|true|false|["'][^"']*["'])/.freeze # :nodoc:
8
+
6
9
  Error = Class.new(StandardError)
7
10
  HTTPError = Class.new(Error)
8
11
  ServiceError = Error.new(Error)
@@ -51,7 +54,7 @@ class Importmap::Packager
51
54
  end
52
55
 
53
56
  def packaged?(package)
54
- importmap.match(/^pin ["']#{package}["'].*$/)
57
+ importmap.match(Importmap::Map.pin_line_regexp_for(package))
55
58
  end
56
59
 
57
60
  def download(package, url)
@@ -65,14 +68,57 @@ class Importmap::Packager
65
68
  remove_package_from_importmap(package)
66
69
  end
67
70
 
71
+ def extract_existing_pin_options(packages)
72
+ return {} unless @importmap_path.exist?
73
+
74
+ packages = Array(packages)
75
+
76
+ all_package_options = build_package_options_lookup(importmap.lines)
77
+
78
+ packages.to_h do |package|
79
+ [package, all_package_options[package] || {}]
80
+ end
81
+ end
82
+
68
83
  private
84
+ def build_package_options_lookup(lines)
85
+ lines.each_with_object({}) do |line, package_options|
86
+ match = line.strip.match(PIN_REGEX)
87
+
88
+ if match
89
+ package_name = match[1]
90
+ options_part = match[2]
91
+
92
+ preload_match = options_part.match(PRELOAD_OPTION_REGEXP)
93
+
94
+ if preload_match
95
+ preload = preload_from_string(preload_match[1])
96
+ package_options[package_name] = { preload: preload }
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ def preload_from_string(value)
103
+ case value
104
+ when "true"
105
+ true
106
+ when "false"
107
+ false
108
+ when /^\[.*\]$/
109
+ JSON.parse(value)
110
+ else
111
+ value.gsub(/["']/, "")
112
+ end
113
+ end
114
+
69
115
  def preload(preloads)
70
116
  case Array(preloads)
71
117
  in []
72
118
  ""
73
- in ["true"]
119
+ in ["true"] | [true]
74
120
  %(, preload: true)
75
- in ["false"]
121
+ in ["false"] | [false]
76
122
  %(, preload: false)
77
123
  in [string]
78
124
  %(, preload: "#{string}")
@@ -129,7 +175,7 @@ class Importmap::Packager
129
175
 
130
176
  def remove_package_from_importmap(package)
131
177
  all_lines = File.readlines(@importmap_path)
132
- with_lines_removed = all_lines.grep_v(/pin ["']#{package}["']/)
178
+ with_lines_removed = all_lines.grep_v(Importmap::Map.pin_line_regexp_for(package))
133
179
 
134
180
  File.open(@importmap_path, "w") do |file|
135
181
  with_lines_removed.each { |line| file.write(line) }
@@ -1,3 +1,3 @@
1
1
  module Importmap
2
- VERSION = "2.2.1"
2
+ VERSION = "2.2.2"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: importmap-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.1
4
+ version: 2.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson