Skip to content

Commit 5e25781

Browse files
committed
Keep options when updating packages in importmap
The only option that is relevant to keep is the `preload` option. Not all cases are kept though. If the option is set to a variable or complex expression, it is not kept. Only simple values like `true`, `false`, or a string/array of strings are preserved.
1 parent a151881 commit 5e25781

File tree

4 files changed

+296
-10
lines changed

4 files changed

+296
-10
lines changed

lib/importmap/commands.rb

Lines changed: 19 additions & 8 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,6 +117,16 @@ 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)
120131
new_pin = "#{pin}\n"
121132

lib/importmap/packager.rb

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
class Importmap::Packager
66
PIN_REGEX = /#{Importmap::Map::PIN_REGEX}(.*)/.freeze # :nodoc:
7+
PRELOAD_OPTION_REGEXP = /preload:\s*(\[[^\]]+\]|true|false|["'][^"']*["'])/.freeze # :nodoc:
78

89
Error = Class.new(StandardError)
910
HTTPError = Class.new(Error)
@@ -67,14 +68,57 @@ def remove(package)
6768
remove_package_from_importmap(package)
6869
end
6970

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+
7083
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+
71115
def preload(preloads)
72116
case Array(preloads)
73117
in []
74118
""
75-
in ["true"]
119+
in ["true"] | [true]
76120
%(, preload: true)
77-
in ["false"]
121+
in ["false"] | [false]
78122
%(, preload: false)
79123
in [string]
80124
%(, preload: "#{string}")

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

test/packager_test.rb

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,137 @@ def code() "200" end
7979
assert_equal %(pin "react", preload: "foo" # @17.0.2), @packager.vendored_pin_for("react", "https://cdn/[email protected]", ["foo"])
8080
assert_equal %(pin "react", preload: ["foo", "bar"] # @17.0.2), @packager.vendored_pin_for("react", "https://cdn/[email protected]", ["foo", "bar"])
8181
end
82+
83+
test "extract_existing_pin_options with preload false" do
84+
temp_importmap = create_temp_importmap('pin "package1", preload: false')
85+
packager = Importmap::Packager.new(temp_importmap)
86+
87+
options = extract_options_for_package(packager, "package1")
88+
89+
assert_equal({ preload: false }, options)
90+
end
91+
92+
test "extract_existing_pin_options with preload true" do
93+
temp_importmap = create_temp_importmap('pin "package1", preload: true')
94+
packager = Importmap::Packager.new(temp_importmap)
95+
96+
options = extract_options_for_package(packager, "package1")
97+
98+
assert_equal({ preload: true }, options)
99+
end
100+
101+
test "extract_existing_pin_options with custom preload string" do
102+
temp_importmap = create_temp_importmap('pin "package1", preload: "custom"')
103+
packager = Importmap::Packager.new(temp_importmap)
104+
105+
options = extract_options_for_package(packager, "package1")
106+
107+
assert_equal({ preload: "custom" }, options)
108+
end
109+
110+
test "extract_existing_pin_options with custom preload array" do
111+
temp_importmap = create_temp_importmap('pin "package1", preload: ["custom1", "custom2"]')
112+
packager = Importmap::Packager.new(temp_importmap)
113+
114+
options = extract_options_for_package(packager, "package1")
115+
116+
assert_equal({ preload: ["custom1", "custom2"] }, options)
117+
end
118+
119+
test "extract_existing_pin_options with to option only" do
120+
temp_importmap = create_temp_importmap('pin "package1", to: "custom_path.js"')
121+
packager = Importmap::Packager.new(temp_importmap)
122+
123+
options = extract_options_for_package(packager, "package1")
124+
125+
assert_equal({}, options)
126+
end
127+
128+
test "extract_existing_pin_options with integrity option only" do
129+
temp_importmap = create_temp_importmap('pin "package1", integrity: "sha384-abcdef1234567890"')
130+
packager = Importmap::Packager.new(temp_importmap)
131+
132+
options = extract_options_for_package(packager, "package1")
133+
134+
assert_equal({}, options)
135+
end
136+
137+
test "extract_existing_pin_options with multiple options" do
138+
temp_importmap = create_temp_importmap('pin "package1", to: "path.js", preload: false, integrity: "sha384-abcdef1234567890"')
139+
packager = Importmap::Packager.new(temp_importmap)
140+
141+
options = extract_options_for_package(packager, "package1")
142+
143+
assert_equal({ preload: false }, options)
144+
end
145+
146+
test "extract_existing_pin_options with version comment" do
147+
temp_importmap = create_temp_importmap('pin "package1", preload: false # @2.0.0')
148+
packager = Importmap::Packager.new(temp_importmap)
149+
150+
options = extract_options_for_package(packager, "package1")
151+
152+
assert_equal({ preload: false }, options)
153+
end
154+
155+
test "extract_existing_pin_options with no options" do
156+
temp_importmap = create_temp_importmap('pin "package1"')
157+
packager = Importmap::Packager.new(temp_importmap)
158+
159+
options = extract_options_for_package(packager, "package1")
160+
161+
assert_equal({}, options)
162+
end
163+
164+
test "extract_existing_pin_options with nonexistent package" do
165+
temp_importmap = create_temp_importmap('pin "package1", preload: false')
166+
packager = Importmap::Packager.new(temp_importmap)
167+
168+
options = extract_options_for_package(packager, "nonexistent")
169+
170+
assert_equal({}, options)
171+
end
172+
173+
test "extract_existing_pin_options with nonexistent file" do
174+
packager = Importmap::Packager.new("/nonexistent/path")
175+
176+
options = extract_options_for_package(packager, "package1")
177+
178+
assert_nil options
179+
end
180+
181+
test "extract_existing_pin_options handles multiple packages in one call" do
182+
temp_importmap = create_temp_importmap(<<~PINS)
183+
pin "package1", preload: false
184+
pin "package2", preload: true
185+
pin "package3", preload: "custom"
186+
pin "package4" # no options
187+
PINS
188+
189+
packager = Importmap::Packager.new(temp_importmap)
190+
191+
result = packager.extract_existing_pin_options(["package1", "package2", "package3", "package4", "nonexistent"])
192+
193+
assert_equal({
194+
"package1" => { preload: false },
195+
"package2" => { preload: true },
196+
"package3" => { preload: "custom" },
197+
"package4" => {},
198+
"nonexistent" => {}
199+
}, result)
200+
end
201+
202+
private
203+
204+
def create_temp_importmap(content)
205+
temp_file = Tempfile.new(['importmap', '.rb'])
206+
temp_file.write(content)
207+
temp_file.close
208+
temp_file.path
209+
end
210+
211+
def extract_options_for_package(packager, package_name)
212+
result = packager.extract_existing_pin_options(package_name)
213+
result[package_name]
214+
end
82215
end

0 commit comments

Comments
 (0)