Skip to content

Commit ae67187

Browse files
authored
Merge pull request #310 from rails/rm-update-keep-options
Keep options when updating packages in importmap
2 parents ebed9bd + 5e25781 commit ae67187

File tree

6 files changed

+314
-18
lines changed

6 files changed

+314
-18
lines changed

lib/importmap/commands.rb

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,7 @@ def self.exit_on_failure?
1515
option :preload, type: :string, repeatable: true, desc: "Can be used multiple times"
1616
def pin(*packages)
1717
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])
2519
end
2620
end
2721

@@ -96,7 +90,14 @@ def outdated
9690
desc "update", "Update outdated package pins"
9791
def update
9892
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
100101
else
101102
puts "No outdated packages found"
102103
end
@@ -116,11 +117,23 @@ def npm
116117
@npm ||= Importmap::Npm.new
117118
end
118119

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+
119130
def update_importmap_with_pin(package, pin)
131+
new_pin = "#{pin}\n"
132+
120133
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), new_pin, verbose: false)
122135
else
123-
append_to_file("config/importmap.rb", "#{pin}\n", verbose: false)
136+
append_to_file("config/importmap.rb", new_pin, verbose: false)
124137
end
125138
end
126139

lib/importmap/map.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
class Importmap::Map
44
attr_reader :packages, :directories
55

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+
612
class InvalidFile < StandardError; end
713

814
def initialize

lib/importmap/npm.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
require "json"
44

55
class Importmap::Npm
6-
PIN_REGEX = /^pin ["']([^["']]*)["'].*/
6+
PIN_REGEX = /#{Importmap::Map::PIN_REGEX}.*/.freeze # :nodoc:
77

88
Error = Class.new(StandardError)
99
HTTPError = Class.new(Error)
@@ -17,7 +17,7 @@ def initialize(importmap_path = "config/importmap.rb", vendor_path: "vendor/java
1717
end
1818

1919
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|
2121
outdated_package = OutdatedPackage.new(name: package, current_version: current_version)
2222

2323
if !(response = get_package(package))
@@ -51,7 +51,7 @@ def vulnerable_packages
5151
def packages_with_versions
5252
# We cannot use the name after "pin" because some dependencies are loaded from inside packages
5353
# Eg. pin "buffer", to: "https://ga.jspm.io/npm:@jspm/[email protected]/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"']*))/) |
5555
importmap.scan(/#{PIN_REGEX} #.*@(\d+\.\d+\.\d+(?:[^\s]*)).*$/)
5656

5757
vendored_packages_without_version(with_versions).each do |package, path|
@@ -147,7 +147,7 @@ def vendored_packages_without_version(packages_with_versions)
147147
end
148148

149149
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
151151
match = line.match(regexp)
152152

153153
return unless match

lib/importmap/packager.rb

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
require "json"
44

55
class Importmap::Packager
6+
PIN_REGEX = /#{Importmap::Map::PIN_REGEX}(.*)/.freeze # :nodoc:
7+
PRELOAD_OPTION_REGEXP = /preload:\s*(\[[^\]]+\]|true|false|["'][^"']*["'])/.freeze # :nodoc:
8+
69
Error = Class.new(StandardError)
710
HTTPError = Class.new(Error)
811
ServiceError = Error.new(Error)
@@ -51,7 +54,7 @@ def vendored_pin_for(package, url, preloads = nil)
5154
end
5255

5356
def packaged?(package)
54-
importmap.match(/^pin ["']#{package}["'].*$/)
57+
importmap.match(Importmap::Map.pin_line_regexp_for(package))
5558
end
5659

5760
def download(package, url)
@@ -65,14 +68,57 @@ def remove(package)
6568
remove_package_from_importmap(package)
6669
end
6770

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+
6883
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+
69115
def preload(preloads)
70116
case Array(preloads)
71117
in []
72118
""
73-
in ["true"]
119+
in ["true"] | [true]
74120
%(, preload: true)
75-
in ["false"]
121+
in ["false"] | [false]
76122
%(, preload: false)
77123
in [string]
78124
%(, preload: "#{string}")
@@ -129,7 +175,7 @@ def remove_existing_package_file(package)
129175

130176
def remove_package_from_importmap(package)
131177
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))
133179

134180
File.open(@importmap_path, "w") do |file|
135181
with_lines_removed.each { |line| file.write(line) }

test/commands_test.rb

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,105 @@ class CommandsTest < ActiveSupport::TestCase
5050
assert_equal original, File.read("#{@tmpdir}/dummy/vendor/javascript/md5.js")
5151
end
5252

53+
test "update command preserves preload false option" do
54+
importmap_config('pin "md5", to: "https://cdn.skypack.dev/[email protected]", preload: false')
55+
56+
out, _err = run_importmap_command("update")
57+
58+
assert_includes out, "Pinning"
59+
60+
updated_content = File.read("#{@tmpdir}/dummy/config/importmap.rb")
61+
assert_includes updated_content, "preload: false"
62+
assert_includes updated_content, "# @2.3.0"
63+
end
64+
65+
test "update command preserves preload true option" do
66+
importmap_config('pin "md5", to: "https://cdn.skypack.dev/[email protected]", preload: true')
67+
68+
out, _err = run_importmap_command("update")
69+
70+
assert_includes out, "Pinning"
71+
72+
updated_content = File.read("#{@tmpdir}/dummy/config/importmap.rb")
73+
assert_includes updated_content, "preload: true"
74+
end
75+
76+
test "update command preserves custom preload string option" do
77+
importmap_config('pin "md5", to: "https://cdn.skypack.dev/[email protected]", preload: "custom"')
78+
79+
out, _err = run_importmap_command("update")
80+
81+
assert_includes out, "Pinning"
82+
83+
updated_content = File.read("#{@tmpdir}/dummy/config/importmap.rb")
84+
assert_includes updated_content, 'preload: "custom"'
85+
end
86+
87+
test "update command removes existing integrity" do
88+
importmap_config('pin "md5", to: "https://cdn.skypack.dev/[email protected]", integrity: "sha384-oldintegrity"')
89+
90+
out, _err = run_importmap_command("update")
91+
92+
assert_includes out, "Pinning"
93+
94+
updated_content = File.read("#{@tmpdir}/dummy/config/importmap.rb")
95+
assert_not_includes updated_content, "integrity:"
96+
end
97+
98+
test "update command only keeps preload option" do
99+
importmap_config('pin "md5", to: "https://cdn.skypack.dev/[email protected]", preload: false, integrity: "sha384-oldintegrity"')
100+
101+
out, _err = run_importmap_command("update")
102+
103+
assert_includes out, "Pinning"
104+
105+
updated_content = File.read("#{@tmpdir}/dummy/config/importmap.rb")
106+
assert_includes updated_content, "preload: false"
107+
assert_not_includes updated_content, "to:"
108+
assert_not_includes updated_content, "integrity:"
109+
end
110+
111+
test "update command handles packages with different quote styles" do
112+
importmap_config("pin 'md5', to: 'https://cdn.skypack.dev/[email protected]', preload: false")
113+
114+
out, _err = run_importmap_command("update")
115+
116+
assert_includes out, "Pinning"
117+
118+
updated_content = File.read("#{@tmpdir}/dummy/config/importmap.rb")
119+
assert_includes updated_content, "preload: false"
120+
end
121+
122+
test "update command preserves options with version comments" do
123+
importmap_config('pin "md5", to: "https://cdn.skypack.dev/[email protected]", preload: false # @2.2.0')
124+
125+
out, _err = run_importmap_command("update")
126+
127+
assert_includes out, "Pinning"
128+
129+
updated_content = File.read("#{@tmpdir}/dummy/config/importmap.rb")
130+
assert_includes updated_content, "preload: false"
131+
assert_includes updated_content, "# @2.3.0"
132+
assert_not_includes updated_content, "# @2.2.0"
133+
end
134+
135+
test "update command handles whitespace variations in pin options" do
136+
importmap_config('pin "md5", to: "https://cdn.skypack.dev/[email protected]", preload: false ')
137+
138+
out, _err = run_importmap_command("update")
139+
140+
assert_includes out, "Pinning"
141+
142+
updated_content = File.read("#{@tmpdir}/dummy/config/importmap.rb")
143+
assert_equal 4, updated_content.lines.size
144+
assert_includes updated_content, "preload: false"
145+
end
146+
53147
private
148+
def importmap_config(content)
149+
File.write("#{@tmpdir}/dummy/config/importmap.rb", content)
150+
end
151+
54152
def run_importmap_command(command, *args)
55153
capture_subprocess_io { system("bin/importmap", command, *args, exception: true) }
56154
end

0 commit comments

Comments
 (0)