diff --git a/Gemfile.lock b/Gemfile.lock index cf2b0bb..4056010 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -104,13 +104,13 @@ GEM mini_mime (1.1.2) minitest (5.16.2) nio4r (2.5.8) - nokogiri (1.14.0-aarch64-linux) + nokogiri (1.16.0-aarch64-linux) racc (~> 1.4) - nokogiri (1.14.0-arm64-darwin) + nokogiri (1.16.0-arm64-darwin) racc (~> 1.4) - nokogiri (1.14.0-x86_64-darwin) + nokogiri (1.16.0-x86_64-darwin) racc (~> 1.4) - nokogiri (1.14.0-x86_64-linux) + nokogiri (1.16.0-x86_64-linux) racc (~> 1.4) public_suffix (4.0.7) racc (1.6.2) diff --git a/lib/importmap/commands.rb b/lib/importmap/commands.rb index 350cc42..11f6947 100644 --- a/lib/importmap/commands.rb +++ b/lib/importmap/commands.rb @@ -14,16 +14,9 @@ def self.exit_on_failure? option :from, type: :string, aliases: :f, default: "jspm" def pin(*packages) if imports = packager.import(*packages, env: options[:env], from: options[:from]) - imports.each do |package, url| - puts %(Pinning "#{package}" to #{packager.vendor_path}/#{package}.js via download from #{url}) - packager.download(package, url) - pin = packager.vendored_pin_for(package, url) - - if packager.packaged?(package) - gsub_file("config/importmap.rb", /^pin "#{package}".*$/, pin, verbose: false) - else - append_to_file("config/importmap.rb", "#{pin}\n", verbose: false) - end + imports.each do |package| + puts %(Pinning "#{package.package_name}" to #{packager.vendor_path}/#{package.vendored_package_folder} via download from #{package.base_url}) + package.download end else puts "Couldn't find any packages in #{packages.inspect} on #{options[:from]}" @@ -35,10 +28,10 @@ def pin(*packages) option :from, type: :string, aliases: :f, default: "jspm" def unpin(*packages) if imports = packager.import(*packages, env: options[:env], from: options[:from]) - imports.each do |package, url| - if packager.packaged?(package) - puts %(Unpinning and removing "#{package}") - packager.remove(package) + imports.each do |package| + if packager.packaged?(package.package_name) + puts %(Unpinning and removing "#{package.package_name}") + package.remove end end else diff --git a/lib/importmap/jspm_api.rb b/lib/importmap/jspm_api.rb new file mode 100644 index 0000000..041114f --- /dev/null +++ b/lib/importmap/jspm_api.rb @@ -0,0 +1,91 @@ +class Importmap::JspmApi + Error = Class.new(StandardError) + HTTPError = Class.new(Error) + ServiceError = Error.new(Error) + + singleton_class.attr_accessor :generate_endpoint, :download_endpoint + self.generate_endpoint = "https://api.jspm.io/generate" + self.download_endpoint = "https://api.jspm.io/download" + + def generate(install:, flatten_scope:, env:, provider:) + response = post_json(self.class.generate_endpoint, { + install:, + flattenScope: flatten_scope, + env:, + provider: normalize_provider(provider) + }) + + response_json(response) + end + + def download(versioned_package_names:, provider:, exclude:) + response = post_json("#{self.class.download_endpoint}", { + packages: versioned_package_names, + provider: normalize_provider(provider), + exclude: + }) + + json = response_json(response) + + return {} if json.blank? + + json.transform_values do |package_download_details| + files = package_download_details["files"] + package_uri = URI(package_download_details["pkgUrl"]) + + Net::HTTP.start(package_uri.hostname, { use_ssl: true }) do |http| + files.map do |file| + [ + file, + fetch_file(http, "#{package_uri.path}/#{file}") + ] + end.to_h + end + end + end + + private + def fetch_file(http, path) + response = http.get(path) + + if response.code == "200" + response.body + else + handle_failure_response(response) + end + rescue => error + raise HTTPError, "Unexpected transport error (#{error.class}: #{error.message})" + end + + def response_json(response) + case response.code + when "200" then JSON.parse(response.body) + when "404", "401" then {} + else handle_failure_response(response) + end + end + + def normalize_provider(name) + name.to_s == "jspm" ? "jspm.io" : name.to_s + end + + def post_json(endpoint, body) + Net::HTTP.post(URI(endpoint), body.to_json, "Content-Type" => "application/json") + rescue => error + raise HTTPError, "Unexpected transport error (#{error.class}: #{error.message})" + end + + def handle_failure_response(response) + if error_message = parse_service_error(response) + raise ServiceError, error_message + else + raise HTTPError, "Unexpected response code (#{response.code})" + end + end + + def parse_service_error(response) + JSON.parse(response.body.to_s)["error"] + rescue JSON::ParserError + nil + end +end diff --git a/lib/importmap/package.rb b/lib/importmap/package.rb new file mode 100644 index 0000000..7e8134c --- /dev/null +++ b/lib/importmap/package.rb @@ -0,0 +1,116 @@ +require "importmap/jspm_api" + +class Importmap::Package + attr_reader :base_url, :main_url, :package_name + + def initialize( + package_name:, + main_url:, + packager:, + provider: + ) + @package_name = package_name + @main_url = main_url + @packager = packager + @provider = provider + + @base_url = extract_base_url_from(main_url) + @version = extract_package_version_from(@main_url) + + @main_file = main_url[(base_url.size + 1)..] + end + + def download + @packager.ensure_vendor_directory_exists + remove_existing_package_files + + jspm_api = Importmap::JspmApi.new + + versioned_package_name = "#{@package_name}#{@version}" + + files = jspm_api.download( + versioned_package_names: [versioned_package_name], + provider: @provider, + exclude: ["unused", "readme", "types"] + )[versioned_package_name] + + files.each do |file, downloaded_file| + save_vendored_file(file, downloaded_file) + end + + @packager.pin_package_in_importmap(@package_name, vendored_pin) + end + + def remove + remove_existing_package_files + @packager.remove_package_from_importmap(@package_name) + end + + def vendored_package_folder + @packager.vendor_path.join(folder_name) + end + + private + def vendored_pin + filename = "#{folder_name}/#{@main_file}" + + %(pin "#{package_name}", to: "#{filename}" # #{@version}) + end + + def save_vendored_file(file, source) + file_name = vendored_package_path_for_file(file) + ensure_parent_directories_exist_for(file_name) + + File.open(file_name, "w+") do |vendored_file| + vendored_file.write "// #{@package_name}#{@version}/#{file} downloaded from #{base_url}/#{file}\n\n" + + vendored_file.write remove_sourcemap_comment_from(source).force_encoding("UTF-8") + end + end + + def ensure_parent_directories_exist_for(file) + dir_name = File.dirname(file) + + unless File.directory?(dir_name) + FileUtils.mkdir_p(dir_name) + end + end + + def remove_sourcemap_comment_from(source) + source.gsub(/^\/\/# sourceMappingURL=.*/, "") + end + + def vendored_package_path_for_file(file) + vendored_package_folder.join(file) + end + + def handle_failure_response(response) + if error_message = parse_service_error(response) + raise ServiceError, error_message + else + raise HTTPError, "Unexpected response code (#{response.code})" + end + end + + def parse_service_error(response) + JSON.parse(response.body.to_s)["error"] + rescue JSON::ParserError + nil + end + + def remove_existing_package_files + FileUtils.rm_rf vendored_package_folder + end + + def folder_name + @package_name.gsub("/", "--") + end + + def extract_base_url_from(url) + url.match(/^.+@\d+\.\d+\.\d+/)&.to_a&.first + end + + def extract_package_version_from(url) + url.match(/@\d+\.\d+\.\d+/)&.to_a&.first + end +end diff --git a/lib/importmap/packager.rb b/lib/importmap/packager.rb index 76f0661..fc4c402 100644 --- a/lib/importmap/packager.rb +++ b/lib/importmap/packager.rb @@ -1,16 +1,11 @@ require "net/http" require "uri" require "json" +require "importmap/package" +require "importmap/jspm_api" class Importmap::Packager - Error = Class.new(StandardError) - HTTPError = Class.new(Error) - ServiceError = Error.new(Error) - - singleton_class.attr_accessor :endpoint - self.endpoint = URI("https://api.jspm.io/generate") - - attr_reader :vendor_path + attr_reader :vendor_path, :importmap_path def initialize(importmap_path = "config/importmap.rb", vendor_path: "vendor/javascript") @importmap_path = Pathname.new(importmap_path) @@ -18,132 +13,57 @@ def initialize(importmap_path = "config/importmap.rb", vendor_path: "vendor/java end def import(*packages, env: "production", from: "jspm") - response = post_json({ - "install" => Array(packages), - "flattenScope" => true, - "env" => [ "browser", "module", env ], - "provider" => normalize_provider(from) - }) + jspm_api = Importmap::JspmApi.new - case response.code - when "200" then extract_parsed_imports(response) - when "404", "401" then nil - else handle_failure_response(response) - end + response = jspm_api.generate( + install: Array(packages), + flatten_scope: true, + env: [ "browser", "module", env ], + provider: from + ) + + extract_parsed_imports(response, from) end - def pin_for(package, url) - %(pin "#{package}", to: "#{url}") + def packaged?(package_name) + importmap = File.read(@importmap_path) + importmap.match(/^pin ["']#{package_name}["'].*$/) end - def vendored_pin_for(package, url) - filename = package_filename(package) - version = extract_package_version_from(url) + def remove_package_from_importmap(package_name) + all_lines = File.readlines(@importmap_path) + with_lines_removed = all_lines.grep_v(/pin ["']#{package_name}["']/) - if "#{package}.js" == filename - %(pin "#{package}" # #{version}) - else - %(pin "#{package}", to: "#{filename}" # #{version}) + File.open(@importmap_path, "w") do |file| + with_lines_removed.each { |line| file.write(line) } end end - def packaged?(package) - importmap.match(/^pin ["']#{package}["'].*$/) - end - - def download(package, url) - ensure_vendor_directory_exists - remove_existing_package_file(package) - download_package_file(package, url) + def pin_package_in_importmap(package_name, pin) + if packaged?(package_name) + importmap = File.read(@importmap_path) + modified_importmap = importmap.gsub(/^pin "#{package_name}".*$/, pin) + File.open(@importmap_path, "w") { _1.puts modified_importmap } + else + File.write(@importmap_path, "#{pin}\n", mode: "a+") + end end - def remove(package) - remove_existing_package_file(package) - remove_package_from_importmap(package) + def ensure_vendor_directory_exists + FileUtils.mkdir_p @vendor_path end private - def post_json(body) - Net::HTTP.post(self.class.endpoint, body.to_json, "Content-Type" => "application/json") - rescue => error - raise HTTPError, "Unexpected transport error (#{error.class}: #{error.message})" - end - - def normalize_provider(name) - name.to_s == "jspm" ? "jspm.io" : name.to_s - end - - def extract_parsed_imports(response) - JSON.parse(response.body).dig("map", "imports") - end - - def handle_failure_response(response) - if error_message = parse_service_error(response) - raise ServiceError, error_message - else - raise HTTPError, "Unexpected response code (#{response.code})" - end - end - - def parse_service_error(response) - JSON.parse(response.body.to_s)["error"] - rescue JSON::ParserError - nil - end - - def importmap - @importmap ||= File.read(@importmap_path) - end - - - def ensure_vendor_directory_exists - FileUtils.mkdir_p @vendor_path - end - - def remove_existing_package_file(package) - FileUtils.rm_rf vendored_package_path(package) - end - - def remove_package_from_importmap(package) - all_lines = File.readlines(@importmap_path) - with_lines_removed = all_lines.grep_v(/pin ["']#{package}["']/) - - File.open(@importmap_path, "w") do |file| - with_lines_removed.each { |line| file.write(line) } - end - end - - def download_package_file(package, url) - response = Net::HTTP.get_response(URI(url)) - - if response.code == "200" - save_vendored_package(package, url, response.body) - else - handle_failure_response(response) - end - end - - def save_vendored_package(package, url, source) - File.open(vendored_package_path(package), "w+") do |vendored_package| - vendored_package.write "// #{package}#{extract_package_version_from(url)} downloaded from #{url}\n\n" - - vendored_package.write remove_sourcemap_comment_from(source).force_encoding("UTF-8") + def extract_parsed_imports(response, provider) + imports = response.dig("map", "imports") + + imports&.map do |package, url| + Importmap::Package.new( + package_name: package, + main_url: url, + packager: self, + provider: + ) end end - - def remove_sourcemap_comment_from(source) - source.gsub(/^\/\/# sourceMappingURL=.*/, "") - end - - def vendored_package_path(package) - @vendor_path.join(package_filename(package)) - end - - def package_filename(package) - package.gsub("/", "--") + ".js" - end - - def extract_package_version_from(url) - url.match(/@\d+\.\d+\.\d+/)&.to_a&.first - end end diff --git a/test/jspm_api_integration_test.rb b/test/jspm_api_integration_test.rb new file mode 100644 index 0000000..c965446 --- /dev/null +++ b/test/jspm_api_integration_test.rb @@ -0,0 +1,86 @@ +require "test_helper" +require "importmap/jspm_api" + +class Importmap::JspmApiIntegrationTest < ActiveSupport::TestCase + setup do + @jspm_api = Importmap::JspmApi.new + end + + test "#download when given a valid input" do + result = @jspm_api.download(versioned_package_names: ["@popperjs/core@2.11.8"], provider: "jspm.io", exclude: []) + + assert result["@popperjs/core@2.11.8"].keys.include?("lib/dom-utils/getWindow.js") + assert result["@popperjs/core@2.11.8"].keys.include?("lib/index.js") + + result = @jspm_api.download(versioned_package_names: ["@popperjs/core@2.11.8"], provider: "jspm", exclude: []) + + assert result["@popperjs/core@2.11.8"].keys.include?("lib/dom-utils/getWindow.js") + assert result["@popperjs/core@2.11.8"].keys.include?("lib/index.js") + end + + test "#download when given a bad package" do + result = @jspm_api.download(versioned_package_names: ["@popperjs/corenoversion"], provider: "jspm.io", exclude: []) + + assert_equal result, {} + end + + test "#download when given a bad provider" do + result = @jspm_api.download(versioned_package_names: ["@popperjs/corenoversion"], provider: "jspmfoobarbaz", exclude: []) + + assert_equal result, {} + end + + test "#download when endpoint is incorrect" do + original_endpoint = Importmap::JspmApi.download_endpoint + Importmap::JspmApi.download_endpoint = URI("https://invalid./error") + + assert_raises(Importmap::JspmApi::HTTPError) do + @jspm_api.download(versioned_package_names: ["@popperjs/core@2.11.8"], provider: "jspm.io", exclude: []) + end + ensure + Importmap::JspmApi.download_endpoint = original_endpoint + end + + test "#generate when given valid input" do + response = @jspm_api.generate( + install: "tippy.js", + flatten_scope: true, + env: [ "browser", "module", nil ], + provider: "jspm.io" + ) + + expected_imports = { + "tippy.js" => "https://ga.jspm.io/npm:tippy.js@6.3.7/dist/tippy.esm.js", + "@popperjs/core" => "https://ga.jspm.io/npm:@popperjs/core@2.11.8/lib/index.js" + } + + assert_equal expected_imports, response.dig("map", "imports") + end + + test "#generate when given non existent package" do + response = @jspm_api.generate( + install: "tippy.jsbutnotreallyalibrary", + flatten_scope: true, + env: nil, + provider: "jspm.io" + ) + + assert_equal response, {} + end + + test "#generate when endpoint is incorrect" do + original_endpoint = Importmap::JspmApi.generate_endpoint + Importmap::JspmApi.generate_endpoint = URI("https://invalid./error") + + assert_raises(Importmap::JspmApi::HTTPError) do + @jspm_api.generate( + install: "tippy.jsbutnotreallyalibrary", + flatten_scope: true, + env: nil, + provider: "jspm.io" + ) + end + ensure + Importmap::JspmApi.generate_endpoint = original_endpoint + end +end diff --git a/test/packager_integration_test.rb b/test/packager_integration_test.rb index 3600065..fdf008c 100644 --- a/test/packager_integration_test.rb +++ b/test/packager_integration_test.rb @@ -5,7 +5,13 @@ class Importmap::PackagerIntegrationTest < ActiveSupport::TestCase setup { @packager = Importmap::Packager.new(Rails.root.join("config/importmap.rb")) } test "successful import against live service" do - assert_equal "https://ga.jspm.io/npm:react@17.0.2/index.js", @packager.import("react@17.0.2")["react"] + results = @packager.import("react@17.0.2") + + react_result = results.find { _1.package_name == "react" } + object_assign_result = results.find { _1.package_name == "object-assign" } + + assert_equal("https://ga.jspm.io/npm:react@17.0.2/index.js", react_result.main_url) + assert_equal("https://ga.jspm.io/npm:object-assign@4.1.1/index.js", object_assign_result.main_url) end test "missing import against live service" do @@ -13,36 +19,96 @@ class Importmap::PackagerIntegrationTest < ActiveSupport::TestCase end test "failed request against live bad domain" do - original_endpoint = Importmap::Packager.endpoint - Importmap::Packager.endpoint = URI("https://invalid./error") + original_endpoint = Importmap::JspmApi.generate_endpoint + Importmap::JspmApi.generate_endpoint = URI("https://invalid./error") - assert_raises(Importmap::Packager::HTTPError) do + assert_raises(Importmap::JspmApi::HTTPError) do @packager.import("missing-package-that-doesnt-exist@17.0.2") end ensure - Importmap::Packager.endpoint = original_endpoint + Importmap::JspmApi.generate_endpoint = original_endpoint end test "successful downloads from live service" do Dir.mktmpdir do |vendor_dir| - @packager = Importmap::Packager.new \ - Rails.root.join("config/importmap.rb"), - vendor_path: Pathname.new(vendor_dir) - - package_url = "https://ga.jspm.io/npm:@github/webauthn-json@0.5.7/dist/main/webauthn-json.js" - @packager.download("@github/webauthn-json", package_url) - vendored_package_file = Pathname.new(vendor_dir).join("@github--webauthn-json.js") - assert File.exist?(vendored_package_file) - assert_equal "// @github/webauthn-json@0.5.7 downloaded from #{package_url}", File.readlines(vendored_package_file).first.strip - - package_url = "https://ga.jspm.io/npm:react@17.0.2/index.js" - vendored_package_file = Pathname.new(vendor_dir).join("react.js") - @packager.download("react", package_url) - assert File.exist?(vendored_package_file) - assert_equal "// react@17.0.2 downloaded from #{package_url}", File.readlines(vendored_package_file).first.strip - - @packager.remove("react") - assert_not File.exist?(Pathname.new(vendor_dir).join("react.js")) + importmap_path = Pathname.new(vendor_dir).join("importmap.rb") + + File.new(importmap_path, "w").close + + @packager = Importmap::Packager.new( + importmap_path, + vendor_path: Pathname.new(vendor_dir), + ) + + packages = @packager.import("react@17.0.2") + packages.each(&:download) + + vendored_file = Pathname.new(vendor_dir).join("react/cjs/react.production.min.js") + assert_equal "// react@17.0.2/cjs/react.production.min.js downloaded from https://ga.jspm.io/npm:react@17.0.2/cjs/react.production.min.js", + File.readlines(vendored_file).first.strip + vendored_file = Pathname.new(vendor_dir).join("react/index.js") + assert_equal "// react@17.0.2/index.js downloaded from https://ga.jspm.io/npm:react@17.0.2/index.js", + File.readlines(vendored_file).first.strip + vendored_file = Pathname.new(vendor_dir).join("object-assign/index.js") + assert_equal "// object-assign@4.1.1/index.js downloaded from https://ga.jspm.io/npm:object-assign@4.1.1/index.js", + File.readlines(vendored_file).first.strip + + packages.each(&:remove) + + assert_not File.exist?(Pathname.new(vendor_dir).join("react/cjs/react.production.min.js")) + assert_not File.exist?(Pathname.new(vendor_dir).join("react/index.js")) + assert_not File.exist?(Pathname.new(vendor_dir).join("object-assign/index.js")) + + packages = @packager.import("@github/webauthn-json@0.5.7") + packages.each(&:download) + + vendored_file = Pathname.new(vendor_dir).join("@github--webauthn-json/dist/main/webauthn-json.js") + assert_equal "// @github/webauthn-json@0.5.7/dist/main/webauthn-json.js downloaded from https://ga.jspm.io/npm:@github/webauthn-json@0.5.7/dist/main/webauthn-json.js", + File.readlines(vendored_file).first.strip + + packages.each(&:remove) + + assert_not File.exist?(Pathname.new(vendor_dir).join("webauthn-json/dist/main/webauthn-json.js")) + + packages = @packager.import("tippy.js@6.3.7") + packages.each(&:download) + + assert File.exist?(Pathname.new(vendor_dir).join("@popperjs--core/lib/dom-utils/getWindow.js")) + assert File.exist?(Pathname.new(vendor_dir).join("@popperjs--core/lib/index.js")) + assert File.exist?(Pathname.new(vendor_dir).join("tippy.js/dist/tippy.esm.js")) + + packages.each(&:remove) + + assert_not File.exist?(Pathname.new(vendor_dir).join("@popperjs--core/lib/dom-utils/getWindow.js")) + assert_not File.exist?(Pathname.new(vendor_dir).join("@popperjs--core/lib/index.js")) + assert_not File.exist?(Pathname.new(vendor_dir).join("tippy.js/dist/tippy.esm.js")) + end + end + + test "successful importmap.rb updates from live service" do + Dir.mktmpdir do |vendor_dir| + importmap_path = Pathname.new(vendor_dir).join("importmap.rb") + + File.new(importmap_path, "w").close + + @packager = Importmap::Packager.new( + importmap_path, + vendor_path: Pathname.new(vendor_dir), + ) + + packages = @packager.import("react@17.0.2") + packages.each(&:download) + + importmap = <<~RB + pin "react", to: "react/index.js" # @17.0.2 + pin "object-assign", to: "object-assign/index.js" # @4.1.1 + RB + + assert_equal importmap, importmap_path.read + + packages.each(&:remove) + + assert_equal "", importmap_path.read end end end diff --git a/test/packager_single_quotes_test.rb b/test/packager_single_quotes_test.rb index 519d9e7..ce30b42 100644 --- a/test/packager_single_quotes_test.rb +++ b/test/packager_single_quotes_test.rb @@ -16,7 +16,7 @@ class Importmap::PackagerSingleQuotesTest < ActiveSupport::TestCase end test "remove package with single quotes" do - assert @packager.remove("md5") + assert @packager.remove_package_from_importmap("md5") assert_not @packager.packaged?("md5") end end diff --git a/test/packager_test.rb b/test/packager_test.rb index 29ce7f6..ac29946 100644 --- a/test/packager_test.rb +++ b/test/packager_test.rb @@ -8,35 +8,46 @@ class Importmap::PackagerTest < ActiveSupport::TestCase test "successful import with mock" do response = Class.new do def body - { "map" => { "imports" => imports } }.to_json - end - - def imports { - "react" => "https://ga.jspm.io/npm:react@17.0.2/index.js", - "object-assign" => "https://ga.jspm.io/npm:object-assign@4.1.1/index.js" - } + "staticDeps" => [ + "https://ga.jspm.io/npm:react@17.0.2/index.js", + "https://ga.jspm.io/npm:object-assign@4.1.1/index.js" + ], + "dynamicDeps" => [], + "map" => { + "imports" => { + "react" => "https://ga.jspm.io/npm:react@17.0.2/index.js", + "object-assign" => "https://ga.jspm.io/npm:object-assign@4.1.1/index.js" + } + } + }.to_json end def code() "200" end end.new - @packager.stub(:post_json, response) do - assert_equal(response.imports, @packager.import("react@17.0.2")) + Net::HTTP.stub(:post, response) do + results = @packager.import("react@17.0.2") + + react_result = results.find { _1.package_name == "react" } + object_assign_result = results.find { _1.package_name == "object-assign" } + + assert_equal("https://ga.jspm.io/npm:react@17.0.2/index.js", react_result.main_url) + assert_equal("https://ga.jspm.io/npm:object-assign@4.1.1/index.js", object_assign_result.main_url) end end test "missing import with mock" do response = Class.new { def code() "404" end }.new - @packager.stub(:post_json, response) do + Net::HTTP.stub(:post, response) do assert_nil @packager.import("missing-package-that-doesnt-exist@17.0.2") end end test "failed request with mock" do Net::HTTP.stub(:post, proc { raise "Unexpected Error" }) do - assert_raises(Importmap::Packager::HTTPError) do + assert_raises(Importmap::JspmApi::HTTPError) do @packager.import("missing-package-that-doesnt-exist@17.0.2") end end @@ -47,12 +58,50 @@ def code() "200" end assert_not @packager.packaged?("md5-extension") end - test "pin_for" do - assert_equal %(pin "react", to: "https://cdn/react"), @packager.pin_for("react", "https://cdn/react") + test "pin_package_in_importmap" do + Dir.mktmpdir do |vendor_dir| + importmap_path = Pathname.new(vendor_dir).join("importmap.rb") + + File.new(importmap_path, "w").close + + packager = Importmap::Packager.new( + importmap_path, + vendor_path: Pathname.new(vendor_dir), + ) + + assert_not packager.packaged?("md5") + + packager.pin_package_in_importmap("md5", %(pin "md5", to: "md5/md5.js" # @2.3.0)) + + assert_equal %(pin "md5", to: "md5/md5.js" # @2.3.0), importmap_path.readlines(chomp: true).first + + packager.pin_package_in_importmap("md5", %(pin "md5", to: "md5/md5.js" # @2.3.5)) + + assert_equal %(pin "md5", to: "md5/md5.js" # @2.3.5), importmap_path.readlines(chomp: true).first + end end - test "vendored_pin_for" do - assert_equal %(pin "react" # @17.0.2), @packager.vendored_pin_for("react", "https://cdn/react@17.0.2") - assert_equal %(pin "javascript/react", to: "javascript--react.js" # @17.0.2), @packager.vendored_pin_for("javascript/react", "https://cdn/react@17.0.2") + + test "remove_package_from_importmap" do + Dir.mktmpdir do |vendor_dir| + importmap_path = Pathname.new(vendor_dir).join("importmap.rb") + + File.new(importmap_path, "w").close + + packager = Importmap::Packager.new( + importmap_path, + vendor_path: Pathname.new(vendor_dir), + ) + + assert_not packager.packaged?("md5") + + packager.pin_package_in_importmap("md5", %(pin "md5", to: "md5/md5.js" # @2.3.0)) + + assert_equal %(pin "md5", to: "md5/md5.js" # @2.3.0), importmap_path.readlines(chomp: true).first + + packager.remove_package_from_importmap("md5") + + assert_nil importmap_path.readlines(chomp: true).first + end end end