diff --git a/lib/importmap/npm.rb b/lib/importmap/npm.rb index 5a2c1e5..7c78fd0 100644 --- a/lib/importmap/npm.rb +++ b/lib/importmap/npm.rb @@ -3,20 +3,22 @@ require "json" class Importmap::Npm + PIN_REGEX = /^pin ["']([^["']]*)["'].*/ + Error = Class.new(StandardError) HTTPError = Class.new(Error) singleton_class.attr_accessor :base_uri self.base_uri = URI("https://registry.npmjs.org") - def initialize(importmap_path = "config/importmap.rb") + def initialize(importmap_path = "config/importmap.rb", vendor_path: "vendor/javascript") @importmap_path = Pathname.new(importmap_path) + @vendor_path = Pathname.new(vendor_path) end def outdated_packages packages_with_versions.each.with_object([]) do |(package, current_version), outdated_packages| - outdated_package = OutdatedPackage.new(name: package, - current_version: current_version) + outdated_package = OutdatedPackage.new(name: package, current_version: current_version) if !(response = get_package(package)) outdated_package.error = 'Response error' @@ -36,10 +38,12 @@ def outdated_packages def vulnerable_packages get_audit.flat_map do |package, vulnerabilities| vulnerabilities.map do |vulnerability| - VulnerablePackage.new(name: package, - severity: vulnerability['severity'], - vulnerable_versions: vulnerability['vulnerable_versions'], - vulnerability: vulnerability['title']) + VulnerablePackage.new( + name: package, + severity: vulnerability['severity'], + vulnerable_versions: vulnerability['vulnerable_versions'], + vulnerability: vulnerability['title'] + ) end end.sort_by { |p| [p.name, p.severity] } end @@ -47,17 +51,20 @@ def vulnerable_packages def packages_with_versions # We cannot use the name after "pin" because some dependencies are loaded from inside packages # Eg. pin "buffer", to: "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.19/nodelibs/browser/buffer.js" + with_versions = importmap.scan(/^pin .*(?<=npm:|npm\/|skypack\.dev\/|unpkg\.com\/)(.*)(?=@\d+\.\d+\.\d+)@(\d+\.\d+\.\d+(?:[^\/\s["']]*)).*$/) | + importmap.scan(/#{PIN_REGEX} #.*@(\d+\.\d+\.\d+(?:[^\s]*)).*$/) + + vendored_packages_without_version(with_versions).each do |package, path| + $stdout.puts "Ignoring #{package} (#{path}) since no version is specified in the importmap" + end - importmap.scan(/^pin .*(?<=npm:|npm\/|skypack\.dev\/|unpkg\.com\/)(.*)(?=@\d+\.\d+\.\d+)@(\d+\.\d+\.\d+(?:[^\/\s["']]*)).*$/) | - importmap.scan(/^pin ["']([^["']]*)["'].* #.*@(\d+\.\d+\.\d+(?:[^\s]*)).*$/) + with_versions end private OutdatedPackage = Struct.new(:name, :current_version, :latest_version, :error, keyword_init: true) VulnerablePackage = Struct.new(:name, :severity, :vulnerable_versions, :vulnerability, keyword_init: true) - - def importmap @importmap ||= File.read(@importmap_path) end @@ -130,4 +137,27 @@ def post_json(uri, body) rescue => error raise HTTPError, "Unexpected transport error (#{error.class}: #{error.message})" end + + def vendored_packages_without_version(packages_with_versions) + versioned_packages = packages_with_versions.map(&:first).to_set + + importmap + .lines + .filter_map { |line| find_unversioned_vendored_package(line, versioned_packages) } + end + + def find_unversioned_vendored_package(line, versioned_packages) + regexp = line.include?("to:")? /#{PIN_REGEX}to: ["']([^["']]*)["'].*/ : PIN_REGEX + match = line.match(regexp) + + return unless match + + package, filename = match.captures + filename ||= "#{package}.js" + + return if versioned_packages.include?(package) + + path = File.join(@vendor_path, filename) + [package, path] if File.exist?(path) + end end diff --git a/test/fixtures/files/import_map_without_cdn_and_versions.rb b/test/fixtures/files/import_map_without_cdn_and_versions.rb new file mode 100644 index 0000000..f53f423 --- /dev/null +++ b/test/fixtures/files/import_map_without_cdn_and_versions.rb @@ -0,0 +1,2 @@ +pin "foo", preload: true +pin "@bar/baz", to: "baz.js", preload: true diff --git a/test/npm_test.rb b/test/npm_test.rb index fff3a4b..c5a97ad 100644 --- a/test/npm_test.rb +++ b/test/npm_test.rb @@ -46,24 +46,21 @@ class Importmap::NpmTest < ActiveSupport::TestCase end end - test "missing outdated packages with mock" do - response = { "error" => "Not found" }.to_json + test "warns (and ignores) vendored packages without version" do + Dir.mktmpdir do |vendor_path| + foo_path = create_vendored_file(vendor_path, "foo.js") + baz_path = create_vendored_file(vendor_path, "baz.js") - @npm.stub(:get_json, response) do - outdated_packages = @npm.outdated_packages + npm = Importmap::Npm.new(file_fixture("import_map_without_cdn_and_versions.rb"), vendor_path: vendor_path) - assert_equal(1, outdated_packages.size) - assert_equal('md5', outdated_packages[0].name) - assert_equal('2.2.0', outdated_packages[0].current_version) - assert_equal('Not found', outdated_packages[0].error) - end - end + outdated_packages = [] + stdout, _stderr = capture_io { outdated_packages = npm.outdated_packages } - test "failed outdated packages request with exception" do - Net::HTTP.stub(:start, proc { raise "Unexpected Error" }) do - assert_raises(Importmap::Npm::HTTPError) do - @npm.outdated_packages - end + assert_equal(<<~OUTPUT, stdout) + Ignoring foo (#{foo_path}) since no version is specified in the importmap + Ignoring @bar/baz (#{baz_path}) since no version is specified in the importmap + OUTPUT + assert_equal(0, outdated_packages.size) end end @@ -142,4 +139,10 @@ def code() "200" end assert_equal('version not found', outdated_packages[0].latest_version) end end + + def create_vendored_file(dir, name) + path = File.join(dir, name) + File.write(path, "console.log(123)") + path + end end